Skip to content

Commit 0a9440f

Browse files
committed
Support mapping nested bean properties to current target
When "." is used then the unmapped target properties inspection will use the implicitly mapped properties to decide which target properties are defined Fixes #58
1 parent 2115e25 commit 0a9440f

File tree

9 files changed

+389
-52
lines changed

9 files changed

+389
-52
lines changed

change-notes.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<html>
2-
<h2>1.2.1</h2>
32
<h2>1.2.2</h2>
43
<ul>
4+
<li>Support for mapping nested bean properties to current target</li>
55
<li>Bug fix: ClassCastException in language injection in expressions</li>
66
</ul>
7+
<h2>1.2.1</h2>
78
<ul>
89
<li>Support code completion in Mapping#expression and Mapping#defaultExpression</li>
910
<li>Support meta annotations for when looking for unmapped target properties</li>

src/main/java/org/mapstruct/intellij/inspection/UnmappedTargetPropertiesInspection.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import static org.mapstruct.intellij.util.MapstructUtil.isMapper;
4343
import static org.mapstruct.intellij.util.MapstructUtil.isMapperConfig;
4444
import static org.mapstruct.intellij.util.SourceUtils.findAllSourceProperties;
45+
import static org.mapstruct.intellij.util.TargetUtils.findAllSourcePropertiesForCurrentTarget;
4546
import static org.mapstruct.intellij.util.TargetUtils.findAllTargetProperties;
4647

4748
/**
@@ -81,6 +82,20 @@ public void visitMethod(PsiMethod method) {
8182
.collect( Collectors.toSet() );
8283
allTargetProperties.removeAll( definedTargets );
8384

85+
if ( definedTargets.contains( "." ) ) {
86+
// If there is a defined current target then we need to remove all implicit mapped properties for
87+
// the target source
88+
89+
Set<String> currentTargetSourceProperties =
90+
findAllSourcePropertiesForCurrentTarget(
91+
method,
92+
mapStructVersion
93+
)
94+
.collect( Collectors.toSet() );
95+
96+
allTargetProperties.removeAll( currentTargetSourceProperties );
97+
}
98+
8499
//TODO maybe we need to improve this by more granular extraction
85100
Set<String> sourceProperties = findAllSourceProperties( method );
86101
allTargetProperties.removeAll( sourceProperties );

src/main/java/org/mapstruct/intellij/util/MapstructAnnotationUtils.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
*/
66
package org.mapstruct.intellij.util;
77

8+
import java.util.Collections;
9+
import java.util.Objects;
810
import java.util.Optional;
11+
import java.util.stream.Stream;
912

1013
import com.intellij.codeInsight.AnnotationUtil;
14+
import com.intellij.codeInsight.MetaAnnotationUtil;
1115
import com.intellij.openapi.command.WriteCommandAction;
1216
import com.intellij.openapi.command.undo.UndoUtil;
1317
import com.intellij.openapi.module.LanguageLevelUtil;
@@ -18,6 +22,8 @@
1822
import com.intellij.pom.java.LanguageLevel;
1923
import com.intellij.psi.JavaPsiFacade;
2024
import com.intellij.psi.PsiAnnotation;
25+
import com.intellij.psi.PsiAnnotationMemberValue;
26+
import com.intellij.psi.PsiArrayInitializerMemberValue;
2127
import com.intellij.psi.PsiElement;
2228
import com.intellij.psi.PsiFile;
2329
import com.intellij.psi.PsiMethod;
@@ -26,8 +32,11 @@
2632
import com.intellij.util.IncorrectOperationException;
2733
import org.jetbrains.annotations.NotNull;
2834

35+
import static com.intellij.codeInsight.AnnotationUtil.findAnnotation;
36+
import static com.intellij.codeInsight.AnnotationUtil.findDeclaredAttribute;
2937
import static com.intellij.codeInsight.intention.AddAnnotationPsiFix.addPhysicalAnnotation;
3038
import static com.intellij.codeInsight.intention.AddAnnotationPsiFix.removePhysicalAnnotations;
39+
import static org.mapstruct.intellij.util.MapstructUtil.MAPPING_ANNOTATION_FQN;
3140

3241
/**
3342
* Utils for working with mapstruct annotation.
@@ -222,4 +231,59 @@ private static boolean canUseRepeatableMapping(PsiElement psiElement) {
222231
&& LanguageLevelUtil.getEffectiveLanguageLevel( module ).isAtLeast( LanguageLevel.JDK_1_8 )
223232
&& MapstructUtil.isMapStructJdk8Present( module );
224233
}
234+
235+
public static Stream<PsiAnnotation> findAllDefinedMappingAnnotations(@NotNull PsiMethod method,
236+
MapStructVersion mapStructVersion) {
237+
//TODO cache
238+
Stream<PsiAnnotation> mappingsAnnotations = Stream.empty();
239+
PsiAnnotation mappings = findAnnotation( method, true, MapstructUtil.MAPPINGS_ANNOTATION_FQN );
240+
if ( mappings != null ) {
241+
//TODO maybe there is a better way to do this, but currently I don't have that much knowledge
242+
PsiNameValuePair mappingsValue = findDeclaredAttribute( mappings, null );
243+
if ( mappingsValue != null && mappingsValue.getValue() instanceof PsiArrayInitializerMemberValue ) {
244+
mappingsAnnotations = Stream.of( ( (PsiArrayInitializerMemberValue) mappingsValue.getValue() )
245+
.getInitializers() )
246+
.filter( MapstructAnnotationUtils::isMappingPsiAnnotation )
247+
.map( memberValue -> (PsiAnnotation) memberValue );
248+
}
249+
else if ( mappingsValue != null && mappingsValue.getValue() instanceof PsiAnnotation ) {
250+
mappingsAnnotations = Stream.of( (PsiAnnotation) mappingsValue.getValue() );
251+
}
252+
}
253+
254+
Stream<PsiAnnotation> mappingAnnotations = findMappingAnnotations( method, mapStructVersion );
255+
256+
return Stream.concat( mappingAnnotations, mappingsAnnotations );
257+
}
258+
259+
private static Stream<PsiAnnotation> findMappingAnnotations(@NotNull PsiMethod method,
260+
MapStructVersion mapStructVersion) {
261+
if ( mapStructVersion.isConstructorSupported() ) {
262+
// Meta annotations support was added when constructor support was added
263+
return MetaAnnotationUtil.findMetaAnnotations( method, Collections.singleton( MAPPING_ANNOTATION_FQN ) );
264+
}
265+
return Stream.of( method.getModifierList().getAnnotations() )
266+
.filter( MapstructAnnotationUtils::isMappingAnnotation );
267+
}
268+
269+
/**
270+
* @param memberValue that needs to be checked
271+
*
272+
* @return {@code true} if the {@code memberValue} is the {@link org.mapstruct.Mapping} {@link PsiAnnotation},
273+
* {@code false} otherwise
274+
*/
275+
private static boolean isMappingPsiAnnotation(PsiAnnotationMemberValue memberValue) {
276+
return memberValue instanceof PsiAnnotation
277+
&& isMappingAnnotation( (PsiAnnotation) memberValue );
278+
}
279+
280+
/**
281+
* @param psiAnnotation that needs to be checked
282+
*
283+
* @return {@code true} if the {@code psiAnnotation} is the {@link org.mapstruct.Mapping} annotation, {@code
284+
* false} otherwise
285+
*/
286+
private static boolean isMappingAnnotation(PsiAnnotation psiAnnotation) {
287+
return Objects.equals( psiAnnotation.getQualifiedName(), MAPPING_ANNOTATION_FQN );
288+
}
225289
}

src/main/java/org/mapstruct/intellij/util/SourceUtils.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,18 @@ public static PsiType getParameterType(@NotNull PsiParameter parameter) {
8686
return canDescendIntoType( parameter.getType() ) ? parameter.getType() : null;
8787
}
8888

89+
public static Map<String, Pair<? extends PsiElement, PsiSubstitutor>> publicReadAccessors(
90+
@Nullable PsiElement psiElement) {
91+
if ( psiElement instanceof PsiMethod ) {
92+
return publicReadAccessors( ( (PsiMethod) psiElement ).getReturnType() );
93+
}
94+
else if ( psiElement instanceof PsiParameter ) {
95+
return publicReadAccessors( ( (PsiParameter) psiElement ).getType() );
96+
}
97+
98+
return Collections.emptyMap();
99+
}
100+
89101
/**
90102
* Extract all public read accessors (public getters and fields)
91103
* with their psi substitutors from the given {@code psiType}

src/main/java/org/mapstruct/intellij/util/TargetUtils.java

Lines changed: 22 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,9 @@
1919
import java.util.stream.Stream;
2020

2121
import com.intellij.codeInsight.AnnotationUtil;
22-
import com.intellij.codeInsight.MetaAnnotationUtil;
2322
import com.intellij.lang.jvm.JvmModifier;
2423
import com.intellij.openapi.util.Pair;
2524
import com.intellij.psi.PsiAnnotation;
26-
import com.intellij.psi.PsiAnnotationMemberValue;
27-
import com.intellij.psi.PsiArrayInitializerMemberValue;
2825
import com.intellij.psi.PsiClass;
2926
import com.intellij.psi.PsiElement;
3027
import com.intellij.psi.PsiJavaCodeReferenceElement;
@@ -34,13 +31,12 @@
3431
import com.intellij.psi.PsiParameter;
3532
import com.intellij.psi.PsiSubstitutor;
3633
import com.intellij.psi.PsiType;
34+
import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry;
3735
import com.intellij.psi.util.PsiUtil;
3836
import org.jetbrains.annotations.NotNull;
3937
import org.jetbrains.annotations.Nullable;
4038

41-
import static com.intellij.codeInsight.AnnotationUtil.findAnnotation;
42-
import static com.intellij.codeInsight.AnnotationUtil.findDeclaredAttribute;
43-
import static org.mapstruct.intellij.util.MapstructUtil.MAPPING_ANNOTATION_FQN;
39+
import static org.mapstruct.intellij.util.MapstructAnnotationUtils.findAllDefinedMappingAnnotations;
4440
import static org.mapstruct.intellij.util.MapstructUtil.canDescendIntoType;
4541
import static org.mapstruct.intellij.util.MapstructUtil.isFluentSetter;
4642
import static org.mapstruct.intellij.util.MapstructUtil.publicFields;
@@ -318,60 +314,35 @@ private static boolean hasBuildMethod(@Nullable PsiType builderType, @NotNull Ps
318314
*/
319315
public static Stream<String> findAllDefinedMappingTargets(@NotNull PsiMethod method,
320316
MapStructVersion mapStructVersion) {
321-
//TODO cache
322-
PsiAnnotation mappings = findAnnotation( method, true, MapstructUtil.MAPPINGS_ANNOTATION_FQN );
323-
Stream<PsiAnnotation> mappingsAnnotations = Stream.empty();
324-
if ( mappings != null ) {
325-
//TODO maybe there is a better way to do this, but currently I don't have that much knowledge
326-
PsiNameValuePair mappingsValue = findDeclaredAttribute( mappings, null );
327-
if ( mappingsValue != null && mappingsValue.getValue() instanceof PsiArrayInitializerMemberValue ) {
328-
mappingsAnnotations = Stream.of( ( (PsiArrayInitializerMemberValue) mappingsValue.getValue() )
329-
.getInitializers() )
330-
.filter( TargetUtils::isMappingPsiAnnotation )
331-
.map( memberValue -> (PsiAnnotation) memberValue );
332-
}
333-
else if ( mappingsValue != null && mappingsValue.getValue() instanceof PsiAnnotation ) {
334-
mappingsAnnotations = Stream.of( (PsiAnnotation) mappingsValue.getValue() );
335-
}
336-
}
337-
338-
Stream<PsiAnnotation> mappingAnnotations = findMappingAnnotations( method, mapStructVersion );
339-
340-
return Stream.concat( mappingAnnotations, mappingsAnnotations )
317+
return findAllDefinedMappingAnnotations( method, mapStructVersion )
341318
.map( psiAnnotation -> AnnotationUtil.getDeclaredStringAttributeValue( psiAnnotation, "target" ) )
342319
.filter( Objects::nonNull )
343320
.filter( s -> !s.isEmpty() );
344321
}
345322

346-
private static Stream<PsiAnnotation> findMappingAnnotations(@NotNull PsiMethod method,
347-
MapStructVersion mapStructVersion) {
348-
if ( mapStructVersion.isConstructorSupported() ) {
349-
// Meta annotations support was added when constructor support was added
350-
return MetaAnnotationUtil.findMetaAnnotations( method, Collections.singleton( MAPPING_ANNOTATION_FQN ) );
351-
}
352-
return Stream.of( method.getModifierList().getAnnotations() )
353-
.filter( TargetUtils::isMappingAnnotation );
354-
}
355-
356323
/**
357-
* @param memberValue that needs to be checked
324+
* Find all implicit source properties for all targets mapping to the current target, i.e. ".".
358325
*
359-
* @return {@code true} if the {@code memberValue} is the {@link org.mapstruct.Mapping} {@link PsiAnnotation},
360-
* {@code false} otherwise
361-
*/
362-
private static boolean isMappingPsiAnnotation(PsiAnnotationMemberValue memberValue) {
363-
return memberValue instanceof PsiAnnotation
364-
&& TargetUtils.isMappingAnnotation( (PsiAnnotation) memberValue );
365-
}
366-
367-
/**
368-
* @param psiAnnotation that needs to be checked
326+
* @param method that needs to be checked
327+
* @param mapStructVersion the MapStruct project version
369328
*
370-
* @return {@code true} if the {@code psiAnnotation} is the {@link org.mapstruct.Mapping} annotation, {@code
371-
* false} otherwise
329+
* @return see description
372330
*/
373-
private static boolean isMappingAnnotation(PsiAnnotation psiAnnotation) {
374-
return Objects.equals( psiAnnotation.getQualifiedName(), MAPPING_ANNOTATION_FQN );
331+
public static Stream<String> findAllSourcePropertiesForCurrentTarget(@NotNull PsiMethod method,
332+
MapStructVersion mapStructVersion) {
333+
return findAllDefinedMappingAnnotations( method, mapStructVersion )
334+
.filter( psiAnnotation -> ".".equals( AnnotationUtil.getDeclaredStringAttributeValue(
335+
psiAnnotation,
336+
"target"
337+
) ) )
338+
.map( psiAnnotation -> AnnotationUtil.findDeclaredAttribute( psiAnnotation, "source" ) )
339+
.filter( Objects::nonNull )
340+
.map( PsiNameValuePair::getValue )
341+
.filter( Objects::nonNull )
342+
.map( ReferenceProvidersRegistry::getReferencesFromProviders )
343+
.filter( references -> references.length > 0 )
344+
.map( references -> references[references.length - 1].resolve() )
345+
.flatMap( element -> SourceUtils.publicReadAccessors( element ).keySet().stream() );
375346
}
376347

377348
/**
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright MapStruct Authors.
3+
*
4+
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
package org.mapstruct.intellij.inspection;
7+
8+
import java.util.List;
9+
10+
import com.intellij.codeInsight.intention.IntentionAction;
11+
import org.jetbrains.annotations.NotNull;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
15+
/**
16+
* @author Filip Hrisafov
17+
*/
18+
public class UnmappedCurrentTargetPropertiesInspectionTest extends BaseInspectionTest {
19+
20+
@NotNull
21+
@Override
22+
protected Class<UnmappedTargetPropertiesInspection> getInspection() {
23+
return UnmappedTargetPropertiesInspection.class;
24+
}
25+
26+
@Override
27+
protected void setUp() throws Exception {
28+
super.setUp();
29+
myFixture.copyFileToProject(
30+
"UnmappedCurrentTargetPropertiesData.java",
31+
"org/example/data/UnmappedCurrentTargetPropertiesData.java"
32+
);
33+
}
34+
35+
public void testUnmappedCurrentTargetProperties() {
36+
doTest();
37+
String testName = getTestName( false );
38+
List<IntentionAction> allQuickFixes = myFixture.getAllQuickFixes();
39+
40+
assertThat( allQuickFixes )
41+
.extracting( IntentionAction::getText )
42+
.as( "Intent Text" )
43+
.containsExactly(
44+
// For SingleMappingMapper
45+
"Ignore unmapped target property: 'moreTarget'",
46+
"Add unmapped target property: 'moreTarget'",
47+
"Ignore unmapped target property: 'testName'",
48+
"Add unmapped target property: 'testName'",
49+
"Ignore all unmapped target properties",
50+
51+
// For NoMappingMapper
52+
"Ignore unmapped target property: 'matching'",
53+
"Add unmapped target property: 'matching'",
54+
"Ignore unmapped target property: 'moreTarget'",
55+
"Add unmapped target property: 'moreTarget'",
56+
"Ignore unmapped target property: 'testName'",
57+
"Add unmapped target property: 'testName'",
58+
"Ignore all unmapped target properties",
59+
60+
// For UpdateMapper
61+
"Ignore unmapped target property: 'moreTarget'",
62+
"Add unmapped target property: 'moreTarget'",
63+
"Ignore unmapped target property: 'testName'",
64+
"Add unmapped target property: 'testName'",
65+
"Ignore all unmapped target properties"
66+
);
67+
68+
allQuickFixes.forEach( myFixture::launchAction );
69+
myFixture.checkResultByFile( testName + "_after.java" );
70+
}
71+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright MapStruct Authors.
3+
*
4+
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
7+
import org.mapstruct.Mapper;
8+
import org.mapstruct.Mapping;
9+
import org.mapstruct.MappingTarget;
10+
import org.example.data.UnmappedCurrentTargetPropertiesData.Target;
11+
import org.example.data.UnmappedCurrentTargetPropertiesData.Source;
12+
13+
@Mapper
14+
interface SingleMappingMapper {
15+
16+
@Mapping(target = ".", source = "nested")
17+
Target <warning descr="Unmapped target properties: moreTarget, testName">map</warning>(Source source);
18+
}
19+
20+
@Mapper
21+
interface NoMappingMapper {
22+
23+
Target <warning descr="Unmapped target properties: matching, moreTarget, testName">map</warning>(Source source);
24+
25+
@org.mapstruct.InheritInverseConfiguration
26+
Source reverse(Target target);
27+
}
28+
29+
@Mapper
30+
interface AllMappingMapper {
31+
32+
@Mapping(target = ".", source = "nested")
33+
@Mapping(target = "moreTarget", source = "nested.moreSource")
34+
@Mapping(target = "testName", source = "nested.name")
35+
Target mapWithAllMapping(Source source);
36+
}
37+
38+
@Mapper
39+
interface UpdateMapper {
40+
41+
@Mapping(target = ".", source = "nested")
42+
void <warning descr="Unmapped target properties: moreTarget, testName">update</warning>(@MappingTarget Target target, Source source);
43+
}

0 commit comments

Comments
 (0)