Skip to content

Commit 9c6ab9c

Browse files
authored
#129 Improve support of mapping composition
Support looking up on meta annotations within meta annotations
1 parent 0f0932b commit 9c6ab9c

File tree

6 files changed

+183
-29
lines changed

6 files changed

+183
-29
lines changed

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

Lines changed: 85 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@
66
package org.mapstruct.intellij.util;
77

88
import java.util.ArrayList;
9+
import java.util.Arrays;
910
import java.util.Collections;
11+
import java.util.HashSet;
1012
import java.util.List;
1113
import java.util.Objects;
1214
import java.util.Optional;
15+
import java.util.Set;
1316
import java.util.stream.Collectors;
1417
import java.util.stream.Stream;
1518

1619
import com.intellij.codeInsight.AnnotationUtil;
17-
import com.intellij.codeInsight.MetaAnnotationUtil;
1820
import com.intellij.openapi.command.WriteCommandAction;
1921
import com.intellij.openapi.command.undo.UndoUtil;
2022
import com.intellij.openapi.editor.Editor;
@@ -40,11 +42,13 @@
4042
import com.intellij.psi.PsiFile;
4143
import com.intellij.psi.PsiJavaCodeReferenceElement;
4244
import com.intellij.psi.PsiMethod;
45+
import com.intellij.psi.PsiModifierList;
4346
import com.intellij.psi.PsiModifierListOwner;
4447
import com.intellij.psi.PsiNameValuePair;
4548
import com.intellij.psi.PsiReference;
4649
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
4750
import com.intellij.util.IncorrectOperationException;
51+
import com.intellij.util.containers.ContainerUtil;
4852
import org.jetbrains.annotations.NotNull;
4953
import org.mapstruct.ReportingPolicy;
5054

@@ -281,17 +285,27 @@ private static boolean canUseRepeatableMapping(PsiElement psiElement) {
281285
&& MapstructUtil.isMapStructJdk8Present( module );
282286
}
283287

284-
public static Stream<PsiAnnotation> findAllDefinedMappingAnnotations(@NotNull PsiMethod method,
285-
MapStructVersion mapStructVersion) {
288+
public static Stream<PsiAnnotation> findAllDefinedMappingAnnotations(@NotNull PsiModifierListOwner owner,
289+
MapStructVersion mapStructVersion) {
290+
291+
// Meta annotations support was added when constructor support was added
292+
boolean includeMetaAnnotations = mapStructVersion.isConstructorSupported();
293+
294+
return findAllDefinedMappingAnnotations( owner, includeMetaAnnotations );
295+
}
296+
297+
@NotNull
298+
private static Stream<PsiAnnotation> findAllDefinedMappingAnnotations(@NotNull PsiModifierListOwner owner,
299+
boolean includeMetaAnnotations) {
286300
//TODO cache
287301
Stream<PsiAnnotation> mappingsAnnotations = Stream.empty();
288-
PsiAnnotation mappings = findAnnotation( method, true, MapstructUtil.MAPPINGS_ANNOTATION_FQN );
302+
PsiAnnotation mappings = findAnnotation( owner, true, MapstructUtil.MAPPINGS_ANNOTATION_FQN );
289303
if ( mappings != null ) {
290304
//TODO maybe there is a better way to do this, but currently I don't have that much knowledge
291305
PsiNameValuePair mappingsValue = findDeclaredAttribute( mappings, null );
292306
if ( mappingsValue != null && mappingsValue.getValue() instanceof PsiArrayInitializerMemberValue ) {
293307
mappingsAnnotations = Stream.of( ( (PsiArrayInitializerMemberValue) mappingsValue.getValue() )
294-
.getInitializers() )
308+
.getInitializers() )
295309
.filter( MapstructAnnotationUtils::isMappingPsiAnnotation )
296310
.map( memberValue -> (PsiAnnotation) memberValue );
297311
}
@@ -300,19 +314,61 @@ else if ( mappingsValue != null && mappingsValue.getValue() instanceof PsiAnnota
300314
}
301315
}
302316

303-
Stream<PsiAnnotation> mappingAnnotations = findMappingAnnotations( method, mapStructVersion );
317+
Stream<PsiAnnotation> mappingAnnotations = findMappingAnnotations( owner, includeMetaAnnotations );
304318

305319
return Stream.concat( mappingAnnotations, mappingsAnnotations );
306320
}
307321

308-
private static Stream<PsiAnnotation> findMappingAnnotations(@NotNull PsiMethod method,
309-
MapStructVersion mapStructVersion) {
310-
if ( mapStructVersion.isConstructorSupported() ) {
311-
// Meta annotations support was added when constructor support was added
312-
return MetaAnnotationUtil.findMetaAnnotations( method, Collections.singleton( MAPPING_ANNOTATION_FQN ) );
322+
private static Stream<PsiAnnotation> findMappingAnnotations(@NotNull PsiModifierListOwner method,
323+
boolean includeMetaAnnotations) {
324+
325+
Stream<PsiAnnotation> metaAnnotations = Stream.empty();
326+
327+
if ( includeMetaAnnotations ) {
328+
// do not use MetaAnnotationUtil#findMetaAnnotations since it only finds the first @Mapping annotation
329+
metaAnnotations = findMetaAnnotations( method, new HashSet<>() ).stream();
313330
}
314-
return Stream.of( method.getModifierList().getAnnotations() )
331+
332+
Stream<PsiAnnotation> directAnnotations = Stream.of( method.getModifierList() )
333+
.filter( Objects::nonNull )
334+
.flatMap( psiModifierList -> Arrays.stream( psiModifierList.getAnnotations() ) )
315335
.filter( MapstructAnnotationUtils::isMappingAnnotation );
336+
337+
return Stream.concat( directAnnotations, metaAnnotations );
338+
}
339+
340+
@NotNull
341+
private static Set<PsiAnnotation> findMetaAnnotations(@NotNull PsiModifierListOwner owner,
342+
Set<? super PsiClass> visited) {
343+
344+
Set<PsiAnnotation> result = new HashSet<>();
345+
346+
// to avoid infinite loops, do not include meta annotations at this point
347+
findAllDefinedMappingAnnotations( owner, false ).forEach( result::add );
348+
349+
List<PsiClass> annotationClasses = getResolvedClassesInAnnotationsList( owner );
350+
351+
for ( PsiClass annotationClass : annotationClasses ) {
352+
if ( visited.add( annotationClass ) ) {
353+
result.addAll( findMetaAnnotations( annotationClass, visited ) );
354+
}
355+
}
356+
357+
return result;
358+
}
359+
360+
/**
361+
* copy of private method <code>MetaAnnotationUtil#getResolvedClassesInAnnotationsList(PsiModifierListOwner)</code>
362+
*/
363+
private static List<PsiClass> getResolvedClassesInAnnotationsList(PsiModifierListOwner owner) {
364+
PsiModifierList modifierList = owner.getModifierList();
365+
if ( modifierList != null ) {
366+
return ContainerUtil.mapNotNull(
367+
modifierList.getApplicableAnnotations(),
368+
PsiAnnotation::resolveAnnotationType
369+
);
370+
}
371+
return Collections.emptyList();
316372
}
317373

318374
public static Stream<PsiAnnotation> findAllDefinedValueMappingAnnotations(@NotNull PsiMethod method) {
@@ -483,52 +539,52 @@ private static Stream<PsiClass> findReferencedMappersOfMapperConfig(PsiAnnotatio
483539
}
484540

485541
@NotNull
486-
public static ReportingPolicy getUnmappedTargetPolicy( @NotNull PsiMethod method ) {
542+
public static ReportingPolicy getUnmappedTargetPolicy(@NotNull PsiMethod method) {
487543
PsiAnnotation beanMapping = method.getAnnotation( MapstructUtil.BEAN_MAPPING_FQN );
488-
if (beanMapping != null) {
544+
if ( beanMapping != null ) {
489545
PsiAnnotationMemberValue beanAnnotationOverwrite =
490-
beanMapping.findDeclaredAttributeValue( UNMAPPED_TARGET_POLICY );
491-
if (beanAnnotationOverwrite != null) {
546+
beanMapping.findDeclaredAttributeValue( UNMAPPED_TARGET_POLICY );
547+
if ( beanAnnotationOverwrite != null ) {
492548
return getUnmappedTargetPolicyPolicyFromAnnotation( beanAnnotationOverwrite );
493549
}
494550
}
495551
PsiClass containingClass = method.getContainingClass();
496-
if (containingClass == null) {
552+
if ( containingClass == null ) {
497553
return ReportingPolicy.WARN;
498554
}
499555
return getUnmappedTargetPolicyFromClass( containingClass );
500556
}
501557

502558
@NotNull
503-
private static ReportingPolicy getUnmappedTargetPolicyFromClass( @NotNull PsiClass containingClass ) {
559+
private static ReportingPolicy getUnmappedTargetPolicyFromClass(@NotNull PsiClass containingClass) {
504560
PsiAnnotation mapperAnnotation = containingClass.getAnnotation( MapstructUtil.MAPPER_ANNOTATION_FQN );
505-
if (mapperAnnotation == null) {
561+
if ( mapperAnnotation == null ) {
506562
return ReportingPolicy.WARN;
507563
}
508564

509565
PsiAnnotationMemberValue classAnnotationOverwrite = mapperAnnotation.findDeclaredAttributeValue(
510-
UNMAPPED_TARGET_POLICY );
511-
if (classAnnotationOverwrite != null) {
566+
UNMAPPED_TARGET_POLICY );
567+
if ( classAnnotationOverwrite != null ) {
512568
return getUnmappedTargetPolicyPolicyFromAnnotation( classAnnotationOverwrite );
513569
}
514570
return getUnmappedTargetPolicyFromMapperConfig( mapperAnnotation );
515571
}
516572

517573
@NotNull
518-
private static ReportingPolicy getUnmappedTargetPolicyFromMapperConfig( @NotNull PsiAnnotation mapperAnnotation ) {
519-
PsiModifierListOwner mapperConfigReference = findMapperConfigReference( mapperAnnotation );
574+
private static ReportingPolicy getUnmappedTargetPolicyFromMapperConfig(@NotNull PsiAnnotation mapperAnnotation) {
575+
PsiModifierListOwner mapperConfigReference = findMapperConfigReference( mapperAnnotation );
520576
if ( mapperConfigReference == null ) {
521577
return ReportingPolicy.WARN;
522578
}
523579
PsiAnnotation mapperConfigAnnotation = mapperConfigReference.getAnnotation(
524-
MapstructUtil.MAPPER_CONFIG_ANNOTATION_FQN );
580+
MapstructUtil.MAPPER_CONFIG_ANNOTATION_FQN );
525581

526-
if (mapperConfigAnnotation == null) {
582+
if ( mapperConfigAnnotation == null ) {
527583
return ReportingPolicy.WARN;
528584
}
529585
PsiAnnotationMemberValue configValue =
530-
mapperConfigAnnotation.findDeclaredAttributeValue( UNMAPPED_TARGET_POLICY );
531-
if (configValue == null) {
586+
mapperConfigAnnotation.findDeclaredAttributeValue( UNMAPPED_TARGET_POLICY );
587+
if ( configValue == null ) {
532588
return ReportingPolicy.WARN;
533589
}
534590
return getUnmappedTargetPolicyPolicyFromAnnotation( configValue );
@@ -544,8 +600,8 @@ private static ReportingPolicy getUnmappedTargetPolicyFromMapperConfig( @NotNull
544600
*/
545601
@NotNull
546602
private static ReportingPolicy getUnmappedTargetPolicyPolicyFromAnnotation(
547-
@NotNull PsiAnnotationMemberValue configValue ) {
548-
switch (configValue.getText()) {
603+
@NotNull PsiAnnotationMemberValue configValue) {
604+
switch ( configValue.getText() ) {
549605
case "IGNORE":
550606
case "ReportingPolicy.IGNORE":
551607
return ReportingPolicy.IGNORE;

src/test/java/org/mapstruct/intellij/inspection/UnmappedTargetPropertiesWithMetaAnnotationInspectionTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ protected void setUp() throws Exception {
3535
"MetaMappingIgnoreTestName.java",
3636
"org/example/data/IgnoreTestName.java"
3737
);
38+
39+
myFixture.copyFileToProject(
40+
"MetaMappingMetaAnnotationChained.java",
41+
"org/example/data/AnnotationChained.java"
42+
);
43+
44+
myFixture.copyFileToProject(
45+
"MetaMappingMetaAnnotationChainTarget.java",
46+
"org/example/data/AnnotationChainTarget.java"
47+
);
3848
}
3949

4050
public void testUnmappedTargetPropertiesWithMetaAnnotation() {
@@ -53,4 +63,12 @@ public void testUnmappedTargetPropertiesWithMetaAnnotation() {
5363
allQuickFixes.forEach( myFixture::launchAction );
5464
myFixture.checkResultByFile( testName + "_after.java" );
5565
}
66+
67+
public void testUnmappedTargetPropertiesWithMetaAnnotationChained() {
68+
doTest();
69+
}
70+
71+
public void testUnmappedTargetPropertiesWithMetaAnnotationAndMapping() {
72+
doTest();
73+
}
5674
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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.example.data;
7+
8+
import org.mapstruct.Mapping;
9+
10+
import java.lang.annotation.Retention;
11+
import java.lang.annotation.RetentionPolicy;
12+
13+
@Retention(RetentionPolicy.CLASS)
14+
@Mapping(target = "matching", expression = "java(\"matching-value\")")
15+
@AnnotationChained
16+
public @interface AnnotationChainTarget {
17+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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.example.data;
7+
8+
import org.mapstruct.Mapping;
9+
10+
import java.lang.annotation.Retention;
11+
import java.lang.annotation.RetentionPolicy;
12+
13+
@Retention(RetentionPolicy.CLASS)
14+
@Mapping(target = "testName", ignore = true)
15+
@Mapping(target = "moreTarget", constant = "some-value")
16+
@AnnotationChainTarget
17+
public @interface AnnotationChained {
18+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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.example.data.IgnoreTestName;
10+
import org.example.data.UnmappedTargetPropertiesData.Target;
11+
12+
@Mapper
13+
public interface UnmappedTargetPropertiesWithMetaAnnotationAndMapping {
14+
15+
@IgnoreTestName
16+
@Mapping(target = "moreTarget", constant = "some-value")
17+
@Mapping(target = "matching", constant = "some-value")
18+
Target map(Source source);
19+
20+
public static class Source {
21+
Integer order;
22+
}
23+
24+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.example.data.AnnotationChained;
9+
import org.example.data.UnmappedTargetPropertiesData.Target;
10+
11+
@Mapper
12+
public interface UnmappedTargetPropertiesWithMetaAnnotationChained {
13+
14+
@AnnotationChained
15+
Target map(Source source);
16+
17+
public static class Source {
18+
Integer order;
19+
}
20+
21+
}

0 commit comments

Comments
 (0)