diff --git a/src/main/java/org/mapstruct/intellij/codeinsight/references/BaseReference.java b/src/main/java/org/mapstruct/intellij/codeinsight/references/BaseReference.java index fc0dc97..0f00328 100644 --- a/src/main/java/org/mapstruct/intellij/codeinsight/references/BaseReference.java +++ b/src/main/java/org/mapstruct/intellij/codeinsight/references/BaseReference.java @@ -43,7 +43,7 @@ public abstract class BaseReference extends PsiReferenceBase impleme * @return The mapping method that this reference belongs to */ @Nullable - PsiMethod getMappingMethod() { + public PsiMethod getMappingMethod() { PsiElement element = getElement(); UExpression expression = UastContextKt.toUElement( element, UExpression.class ); if ( expression != null ) { diff --git a/src/main/java/org/mapstruct/intellij/codeinsight/references/BaseValueMappingReference.java b/src/main/java/org/mapstruct/intellij/codeinsight/references/BaseValueMappingReference.java index c8f7129..f129eed 100644 --- a/src/main/java/org/mapstruct/intellij/codeinsight/references/BaseValueMappingReference.java +++ b/src/main/java/org/mapstruct/intellij/codeinsight/references/BaseValueMappingReference.java @@ -47,7 +47,7 @@ public final PsiElement resolve() { @Override @Nullable - PsiMethod getMappingMethod() { + public PsiMethod getMappingMethod() { PsiMethod mappingMethod = super.getMappingMethod(); if ( isNotValueMapping( mappingMethod ) ) { return null; 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 f0ab0a8..83ccc5a 100644 --- a/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructBaseReference.java +++ b/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructBaseReference.java @@ -69,7 +69,8 @@ public final PsiElement resolve() { } if ( previous != null ) { - PsiType psiType = canDescendIntoType( previous.resolvedType() ) ? previous.resolvedType() : null; + PsiType previousResolvedType = previous.resolvedType(); + PsiType psiType = supportsNestedReferenceForType( previousResolvedType ) ? previousResolvedType : null; return psiType == null ? null : resolveInternal( value, psiType ); } @@ -78,6 +79,10 @@ public final PsiElement resolve() { return mappingMethod == null ? null : resolveInternal( value, mappingMethod ); } + protected boolean supportsNestedReferenceForType(@Nullable PsiType psiType) { + return canDescendIntoType( psiType ); + } + /** * Resolved the reference from the {@code value} for the reference {@code psiClass} * 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 29ad9c1..b9b2387 100644 --- a/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructSourceReference.java +++ b/src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructSourceReference.java @@ -57,6 +57,10 @@ PsiElement resolveInternal(@NotNull String value, @NotNull PsiType psiType) { return null; } + if ( MapstructUtil.isMapWithStringKeyType( psiType ) ) { + return psiClass; + } + PsiRecordComponent recordComponent = findRecordComponent( value, psiClass ); if ( recordComponent != null ) { return recordComponent; @@ -78,6 +82,11 @@ PsiElement resolveInternal(@NotNull String value, @NotNull PsiType psiType) { return null; } + @Override + protected boolean supportsNestedReferenceForType(@Nullable PsiType psiType) { + return super.supportsNestedReferenceForType( psiType ) || MapstructUtil.isMapWithStringKeyType( psiType ); + } + @Override PsiElement resolveInternal(@NotNull String value, @NotNull PsiMethod mappingMethod) { PsiParameter[] sourceParameters = MapstructUtil.getSourceParameters( mappingMethod ); diff --git a/src/main/java/org/mapstruct/intellij/inspection/MapstructReferenceInspection.java b/src/main/java/org/mapstruct/intellij/inspection/MapstructReferenceInspection.java index 6984897..d96ecf2 100644 --- a/src/main/java/org/mapstruct/intellij/inspection/MapstructReferenceInspection.java +++ b/src/main/java/org/mapstruct/intellij/inspection/MapstructReferenceInspection.java @@ -13,22 +13,28 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; import com.intellij.psi.PsiLanguageInjectionHost; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiParameter; import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiType; import com.intellij.psi.util.PsiTreeUtil; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.mapstruct.intellij.codeinsight.references.BaseReference; import org.mapstruct.intellij.codeinsight.references.BaseValueMappingReference; +import org.mapstruct.intellij.util.MapstructUtil; /** * Inspection that checks if mapstruct references can be resolved. - * @see BaseReference + * * @author hduelme + * @see BaseReference */ public class MapstructReferenceInspection extends InspectionBase { @Override @NotNull PsiElementVisitor buildVisitorInternal(@NotNull ProblemsHolder holder, boolean isOnTheFly) { - return new MapstructReferenceVisitor(holder); + return new MapstructReferenceVisitor( holder ); } private static class MapstructReferenceVisitor extends PsiElementVisitor { @@ -50,8 +56,10 @@ public void visitElement(@NotNull PsiElement element) { TextRange range = psiReference.getRangeInElement(); if ( range.isEmpty() && range.getStartOffset() == 1 && "\"\"".equals( element.getText() ) ) { String message = ProblemsHolder.unresolvedReferenceMessage( baseReference ); - holder.registerProblem( element, message, ProblemHighlightType.LIKE_UNKNOWN_SYMBOL, - TextRange.create( 0, 2 ) ); + holder.registerProblem( + element, message, ProblemHighlightType.LIKE_UNKNOWN_SYMBOL, + TextRange.create( 0, 2 ) + ); } else if ( shouldRegisterProblem( baseReference ) ) { holder.registerProblem( psiReference ); @@ -67,9 +75,28 @@ private boolean shouldRegisterProblem(BaseReference reference) { return valueMappingReference.getEnumClass() != null; } + if ( hasSingleStringKeyMapSourceParameter( reference.getMappingMethod() ) ) { + // MapStruct allows source values as map keys, even if they are not resolvable as Java properties. + // Therefore, we don't report an unresolved reference problem here. + return false; + } + return !containingClassIsAnnotationType( reference.getElement() ); } + private boolean hasSingleStringKeyMapSourceParameter(@Nullable PsiMethod mappingMethod) { + + if ( mappingMethod != null ) { + PsiParameter[] parameters = MapstructUtil.getSourceParameters( mappingMethod ); + if ( parameters.length == 1 ) { + PsiType parameterType = parameters[0].getType(); + return MapstructUtil.isMapWithStringKeyType( parameterType ); + } + } + + return false; + } + private boolean containingClassIsAnnotationType(PsiElement element) { PsiClass containingClass = PsiTreeUtil.getParentOfType( element, PsiClass.class ); diff --git a/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java b/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java index b0ac84c..a01b04c 100644 --- a/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java +++ b/src/main/java/org/mapstruct/intellij/util/MapstructUtil.java @@ -12,7 +12,7 @@ import java.util.Map; import java.util.function.Function; import java.util.stream.Stream; -import javax.swing.Icon; +import javax.swing.*; import com.intellij.codeInsight.AnnotationUtil; import com.intellij.codeInsight.lookup.LookupElement; @@ -659,4 +659,33 @@ public static boolean isInheritInverseConfiguration(PsiMethod method) { return isAnnotated( method, INHERIT_INVERSE_CONFIGURATION_FQN, AnnotationUtil.CHECK_TYPE ); } + /** + * Checks if the given type is a {@link java.util.Map} with a {@link String} key type. + * + * @param type to be checked + * @return {@code true} if the {@code type} is a {@link java.util.Map} with a {@link String} key type, + * {@code false} otherwise + */ + public static boolean isMapWithStringKeyType(@Nullable PsiType type) { + if ( type == null ) { + return false; + } + + PsiClass psiClass = PsiUtil.resolveClassInType( type ); + if ( psiClass == null || + !CommonClassNames.JAVA_UTIL_MAP.equals( psiClass.getQualifiedName() ) ) { + return false; + } + + if ( !( type instanceof PsiClassType ct ) ) { + return false; + } + + PsiType[] parameters = ct.getParameters(); + if ( parameters.length == 0 ) { + return false; + } + + return parameters[0].equalsToText( CommonClassNames.JAVA_LANG_STRING ); + } } diff --git a/src/test/java/org/mapstruct/intellij/bugs/_243/DisableSourcePropertyInspectionOnMapTest.java b/src/test/java/org/mapstruct/intellij/bugs/_243/DisableSourcePropertyInspectionOnMapTest.java new file mode 100644 index 0000000..1eda638 --- /dev/null +++ b/src/test/java/org/mapstruct/intellij/bugs/_243/DisableSourcePropertyInspectionOnMapTest.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.intellij.bugs._243; + +import org.jetbrains.annotations.NotNull; +import org.mapstruct.intellij.inspection.BaseInspectionTest; +import org.mapstruct.intellij.inspection.MapstructReferenceInspection; + +/** + * @author Oliver Erhart + */ +public class DisableSourcePropertyInspectionOnMapTest extends BaseInspectionTest { + + @Override + protected String getTestDataPath() { + return "testData/bugs/_243"; + } + + @NotNull + @Override + protected Class getInspection() { + return MapstructReferenceInspection.class; + } + + public void testDisableSourcePropertyInspectionOnMapTest() { + doTest(); + } +} diff --git a/testData/bugs/_243/DisableSourcePropertyInspectionOnMapTest.java b/testData/bugs/_243/DisableSourcePropertyInspectionOnMapTest.java new file mode 100644 index 0000000..c3d2812 --- /dev/null +++ b/testData/bugs/_243/DisableSourcePropertyInspectionOnMapTest.java @@ -0,0 +1,54 @@ +/* + * Copyright MapStruct Authors. + * + * Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0 + */ + +import java.time.LocalDate; +import java.util.Map; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; + +@Mapper +abstract class Issue243Mapper { + + @Mapping(source = "exDate", target = "exDate") + @Mapping(source = "payDate", target = "payDate") + public abstract CorporateAction mapWithStringKeyMap(Map rowValues); + + @Mapping(source = "exDate", target = "exDate") + public abstract void updateWithStringKeyMap(Map rowValues, @MappingTarget CorporateAction target); + + @Mapping(source = "exDate", target = "exDate") + public abstract CorporateAction mapWithObjectValueMap(Map rowValues); + + @Mapping(source = "exDate", target = "exDate") + public abstract CorporateAction mapWithIntegerKeyMap(Map rowValues); + + @Mapping(source = "exDate", target = "exDate") + public abstract void updateWithIntegerKeyMap( + Map rowValues, + @MappingTarget CorporateAction target + ); + + @Mapping(source = "exDate", target = "exDate") + @Mapping(source = "payDate", target = "payDate") + public abstract CorporateAction mapWithMultipleSources( + Map rowValues, + LocalDate payDate + ); + + @Mapping(source = "rowValues.exDate", target = "exDate") + @Mapping(source = "payDate", target = "payDate") + public abstract CorporateAction mapWithMultipleSourcesAndMapName( + Map rowValues, + LocalDate payDate + ); +} + +class CorporateAction { + public LocalDate exDate; + public LocalDate payDate; +}