diff --git a/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java b/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java index 5c9295b5..e9860982 100644 --- a/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java +++ b/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java @@ -6,6 +6,7 @@ package org.mapstruct.intellij.util; import java.beans.Introspector; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -108,7 +109,7 @@ private MapstructUtil() { } public static LookupElement[] asLookup(Map> accessors, - Function typeMapper) { + Function typeMapper) { if ( !accessors.isEmpty() ) { LookupElement[] lookupElements = new LookupElement[accessors.size()]; int index = 0; @@ -163,7 +164,7 @@ public static LookupElement asLookup(String propertyName, } public static LookupElement asLookup(String propertyName, @NotNull Pair pair, - Function typeMapper, Icon icon) { + Function typeMapper, Icon icon) { PsiElement member = pair.getFirst(); PsiSubstitutor substitutor = pair.getSecond(); @@ -200,17 +201,35 @@ private static boolean isPublic(@NotNull PsiField field) { public static boolean isPublicModifiable(@NotNull PsiField field) { return isPublicNonStatic( field ) && - !field.hasModifierProperty( PsiModifier.FINAL ); + !field.hasModifierProperty( PsiModifier.FINAL ); } public static boolean isFluentSetter(@NotNull PsiMethod method, PsiType psiType) { return !psiType.getCanonicalText().startsWith( "java.lang" ) && method.getReturnType() != null && !isAdderWithUpperCase4thCharacter( method ) && - TypeConversionUtil.isAssignable( - psiType, - PsiUtil.resolveGenericsClassInType( psiType ).getSubstitutor().substitute( method.getReturnType() ) - ); + isAssignableFromReturnTypeOrSuperTypes( psiType, method.getReturnType() ); + } + + private static boolean isAssignableFromReturnTypeOrSuperTypes(PsiType psiType, @Nullable PsiType returnType) { + + if ( returnType == null ) { + return false; + } + + if ( isAssignableFrom( psiType, returnType ) ) { + return true; + } + + return Arrays.stream( returnType.getSuperTypes() ) + .anyMatch( superType -> isAssignableFrom( psiType, superType ) ); + } + + private static boolean isAssignableFrom(PsiType psiType, @Nullable PsiType returnType) { + return TypeConversionUtil.isAssignable( + psiType, + PsiUtil.resolveGenericsClassInType( psiType ).getSubstitutor().substitute( returnType ) + ); } private static boolean isAdderWithUpperCase4thCharacter(@NotNull PsiMethod method) { diff --git a/src/test/java/org/mapstruct/intellij/inspection/UnmappedSuperBuilderTargetPropertiesInspectionTest.java b/src/test/java/org/mapstruct/intellij/inspection/UnmappedSuperBuilderTargetPropertiesInspectionTest.java new file mode 100644 index 00000000..af8ad73c --- /dev/null +++ b/src/test/java/org/mapstruct/intellij/inspection/UnmappedSuperBuilderTargetPropertiesInspectionTest.java @@ -0,0 +1,62 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.mapstruct.intellij.inspection; + +import java.util.List; + +import com.intellij.codeInsight.intention.IntentionAction; +import org.jetbrains.annotations.NotNull; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Oliver Erhart + */ +public class UnmappedSuperBuilderTargetPropertiesInspectionTest extends BaseInspectionTest { + + @NotNull + @Override + protected Class getInspection() { + return UnmappedTargetPropertiesInspection.class; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject( + "UnmappedSuperBuilderTargetPropertiesData.java", + "org/example/data/UnmappedSuperBuilderTargetPropertiesData.java" + ); + } + + public void testUnmappedSuperBuilderTargetProperties() { + doTest(); + String testName = getTestName( false ); + List allQuickFixes = myFixture.getAllQuickFixes(); + + assertThat( allQuickFixes ) + .extracting( IntentionAction::getText ) + .as( "Intent Text" ) + .containsExactly( + "Ignore unmapped target property: 'testName'", + "Add unmapped target property: 'testName'", + "Ignore unmapped target property: 'moreTarget'", + "Add unmapped target property: 'moreTarget'", + "Ignore unmapped target property: 'moreTarget'", + "Add unmapped target property: 'moreTarget'", + "Ignore unmapped target property: 'testName'", + "Add unmapped target property: 'testName'", + "Ignore all unmapped target properties", + "Ignore unmapped target property: 'testName'", + "Add unmapped target property: 'testName'", + "Ignore unmapped target property: 'moreTarget'", + "Add unmapped target property: 'moreTarget'" + ); + + allQuickFixes.forEach( myFixture::launchAction ); + myFixture.checkResultByFile( testName + "_after.java" ); + } +} diff --git a/testData/inspection/UnmappedSuperBuilderTargetProperties.java b/testData/inspection/UnmappedSuperBuilderTargetProperties.java new file mode 100644 index 00000000..eba027f9 --- /dev/null +++ b/testData/inspection/UnmappedSuperBuilderTargetProperties.java @@ -0,0 +1,64 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ + +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.Mappings; +import org.example.data.UnmappedFluentTargetPropertiesData.Target; +import org.example.data.UnmappedFluentTargetPropertiesData.Source; + +interface NotMapStructMapper { + + Target map(Source source); +} + +@Mapper +interface SingleMappingsMapper { + + @Mappings({ + @Mapping(target = "moreTarget", source = "moreSource") + }) + Target map(Source source); +} + +@Mapper +interface SingleMappingMapper { + + @Mapping(target = "testName", source = "name") + Target map(Source source); +} + +@Mapper +interface NoMappingMapper { + + Target map(Source source); + + @org.mapstruct.InheritInverseConfiguration + Source reverse(Target target); +} + +@Mapper +interface AllMappingMapper { + + @Mapping(target = "testName", source = "name") + @Mapping(target = "moreTarget", source = "moreSource") + Target mapWithAllMapping(Source source); +} + +@Mapper +interface UpdateMapper { + + @Mapping(target = "moreTarget", source = "moreSource") + void update(@MappingTarget Target target, Source source); +} + +@Mapper +interface MultiSourceUpdateMapper { + + void update(@MappingTarget Target moreTarget, Source source, String testName, @Context String matching); +} \ No newline at end of file diff --git a/testData/inspection/UnmappedSuperBuilderTargetPropertiesData.java b/testData/inspection/UnmappedSuperBuilderTargetPropertiesData.java new file mode 100644 index 00000000..b7e272a9 --- /dev/null +++ b/testData/inspection/UnmappedSuperBuilderTargetPropertiesData.java @@ -0,0 +1,133 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ +package org.example.data; + +public class UnmappedFluentTargetPropertiesData { + public static class Source { + + private String name; + private String matching; + private String moreSource; + private String onlyInSource; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMatching() { + return matching; + } + + public void setMatching(String matching) { + this.matching = matching; + } + + public String getMoreSource() { + return moreSource; + } + + public void setMoreSource(String moreSource) { + this.moreSource = moreSource; + } + + public String getOnlyInSource() { + return onlyInSource; + } + + public void setOnlyInSource(String onlyInSource) { + this.onlyInSource = onlyInSource; + } + } + + public static class Target { + + private String testName; + private String matching; + private String moreTarget; + + protected Target(TargetBuilder b) { + this.testName = b.testName; + this.matching = b.matching; + this.moreTarget = b.moreTarget; + } + + public static TargetBuilder builder() { + return new TargetBuilderImpl(); + } + + public String getTestName() { + return this.testName; + } + + public String getMatching() { + return this.matching; + } + + public String getMoreTarget() { + return this.moreTarget; + } + + public void setTestName(String testName) { + this.testName = testName; + } + + public void setMatching(String matching) { + this.matching = matching; + } + + public void setMoreTarget(String moreTarget) { + this.moreTarget = moreTarget; + } + + public static abstract class TargetBuilder> { + private String testName; + private String matching; + private String moreTarget; + + public B testName(String testName) { + this.testName = testName; + return self(); + } + + public B matching(String matching) { + this.matching = matching; + return self(); + } + + public B moreTarget(String moreTarget) { + this.moreTarget = moreTarget; + return self(); + } + + protected abstract B self(); + + public abstract C build(); + + public String toString() { + return "Target.TargetBuilder(testName=" + this.testName + ", matching=" + this.matching + ", moreTarget=" + + this.moreTarget + ")"; + } + } + + private static final class TargetBuilderImpl extends TargetBuilder { + private TargetBuilderImpl() { + } + + protected TargetBuilderImpl self() { + return this; + } + + public Target build() { + return new Target( this ); + } + } + } + +} diff --git a/testData/inspection/UnmappedSuperBuilderTargetProperties_after.java b/testData/inspection/UnmappedSuperBuilderTargetProperties_after.java new file mode 100644 index 00000000..a56c254e --- /dev/null +++ b/testData/inspection/UnmappedSuperBuilderTargetProperties_after.java @@ -0,0 +1,78 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ + +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.Mappings; +import org.example.data.UnmappedFluentTargetPropertiesData.Target; +import org.example.data.UnmappedFluentTargetPropertiesData.Source; + +interface NotMapStructMapper { + + Target map(Source source); +} + +@Mapper +interface SingleMappingsMapper { + + @Mappings({ + @Mapping(target = "moreTarget", source = "moreSource"), + @Mapping(target = "testName", ignore = true), + @Mapping(target = "testName", source = "") + }) + Target map(Source source); +} + +@Mapper +interface SingleMappingMapper { + + @Mapping(target = "moreTarget", source = "") + @Mapping(target = "moreTarget", ignore = true) + @Mapping(target = "testName", source = "name") + Target map(Source source); +} + +@Mapper +interface NoMappingMapper { + + @Mapping(target = "testName", ignore = true) + @Mapping(target = "moreTarget", ignore = true) + @Mapping(target = "testName", source = "") + @Mapping(target = "testName", ignore = true) + @Mapping(target = "moreTarget", source = "") + @Mapping(target = "moreTarget", ignore = true) + Target map(Source source); + + @org.mapstruct.InheritInverseConfiguration + Source reverse(Target target); +} + +@Mapper +interface AllMappingMapper { + + @Mapping(target = "testName", source = "name") + @Mapping(target = "moreTarget", source = "moreSource") + Target mapWithAllMapping(Source source); +} + +@Mapper +interface UpdateMapper { + + @Mapping(target = "testName", source = "") + @Mapping(target = "testName", ignore = true) + @Mapping(target = "moreTarget", source = "moreSource") + void update(@MappingTarget Target target, Source source); +} + +@Mapper +interface MultiSourceUpdateMapper { + + @Mapping(target = "moreTarget", source = "") + @Mapping(target = "moreTarget", ignore = true) + void update(@MappingTarget Target moreTarget, Source source, String testName, @Context String matching); +} \ No newline at end of file