Skip to content

Commit 3047d18

Browse files
authored
#107 Support go to declaration and completion for Mapping#qualifiedByName
1 parent 243f7b8 commit 3047d18

File tree

10 files changed

+475
-0
lines changed

10 files changed

+475
-0
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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.codeinsight.references;
7+
8+
import java.util.Arrays;
9+
import java.util.Objects;
10+
import java.util.stream.Collectors;
11+
import java.util.stream.Stream;
12+
13+
import com.intellij.codeInsight.lookup.LookupElement;
14+
import com.intellij.openapi.util.TextRange;
15+
import com.intellij.openapi.util.text.StringUtil;
16+
import com.intellij.psi.PsiAnnotation;
17+
import com.intellij.psi.PsiClass;
18+
import com.intellij.psi.PsiElement;
19+
import com.intellij.psi.PsiMethod;
20+
import com.intellij.psi.PsiParameter;
21+
import com.intellij.psi.PsiReference;
22+
import com.intellij.psi.PsiType;
23+
import org.jetbrains.annotations.NotNull;
24+
import org.jetbrains.annotations.Nullable;
25+
import org.mapstruct.intellij.util.MapstructUtil;
26+
27+
import static com.intellij.codeInsight.AnnotationUtil.findAnnotation;
28+
import static com.intellij.codeInsight.AnnotationUtil.getStringAttributeValue;
29+
import static org.mapstruct.intellij.util.MapstructAnnotationUtils.findReferencedMapperClasses;
30+
import static org.mapstruct.intellij.util.MapstructUtil.asLookupWithRepresentableText;
31+
32+
/**
33+
* Reference for {@link org.mapstruct.Mapping#qualifiedByName()}.
34+
*
35+
* @author Oliver Erhart
36+
*/
37+
class MapstructMappingQualifiedByNameReference extends MapstructBaseReference {
38+
39+
/**
40+
* Create a new {@link MapstructMappingQualifiedByNameReference} with the provided parameters
41+
*
42+
* @param element the element that the reference belongs to
43+
* @param previousReference the previous reference if there is one (in nested properties for example)
44+
* @param rangeInElement the range that the reference represent in the {@code element}
45+
* @param value the matched value (useful when {@code rangeInElement} is empty)
46+
*/
47+
private MapstructMappingQualifiedByNameReference(PsiElement element,
48+
MapstructMappingQualifiedByNameReference previousReference,
49+
TextRange rangeInElement, String value) {
50+
super( element, previousReference, rangeInElement, value );
51+
}
52+
53+
@Override
54+
PsiElement resolveInternal(@NotNull String value, @NotNull PsiType psiType) {
55+
return null; // not needed
56+
}
57+
58+
@Override
59+
PsiElement resolveInternal(@NotNull String value, @NotNull PsiMethod mappingMethod) {
60+
61+
return findAllNamedMethodsFromThisAndReferencedMappers( mappingMethod )
62+
.filter( a -> Objects.equals( getNamedValue( a ), value ) )
63+
.findAny()
64+
.orElse( null );
65+
}
66+
67+
@Nullable
68+
private String getNamedValue(PsiMethod method) {
69+
70+
PsiAnnotation annotation = findAnnotation( method, true, MapstructUtil.NAMED_ANNOTATION_FQN );
71+
72+
if ( annotation == null ) {
73+
return null;
74+
}
75+
76+
return getStringAttributeValue( annotation, "value" );
77+
}
78+
79+
@NotNull
80+
@Override
81+
Object[] getVariantsInternal(@NotNull PsiType psiType) {
82+
return LookupElement.EMPTY_ARRAY; // not needed
83+
}
84+
85+
@NotNull
86+
@Override
87+
Object[] getVariantsInternal(@NotNull PsiMethod mappingMethod) {
88+
89+
return findAllNamedMethodsFromThisAndReferencedMappers( mappingMethod )
90+
.map( this::methodAsLookup )
91+
.filter( Objects::nonNull )
92+
.toArray();
93+
}
94+
95+
private boolean methodHasReturnType(@NotNull PsiMethod psiMethod) {
96+
return !PsiType.VOID.equals( psiMethod.getReturnType() );
97+
}
98+
99+
@NotNull
100+
private Stream<PsiMethod> findAllNamedMethodsFromThisAndReferencedMappers(@NotNull PsiMethod mappingMethod) {
101+
102+
PsiClass containingClass = mappingMethod.getContainingClass();
103+
if ( containingClass == null ) {
104+
return Stream.empty();
105+
}
106+
107+
Stream<PsiMethod> internalMethods = Stream.of( containingClass.getMethods() )
108+
.filter( MapstructUtil::isNamedMethod );
109+
110+
Stream<PsiMethod> externalMethods = findNamedMethodsInUsedMappers( containingClass );
111+
112+
return Stream.concat( internalMethods, externalMethods )
113+
.filter( this::methodHasReturnType );
114+
}
115+
116+
@NotNull
117+
private Stream<PsiMethod> findNamedMethodsInUsedMappers(@Nullable PsiClass containingClass) {
118+
119+
PsiAnnotation mapperAnnotation = findAnnotation(
120+
containingClass,
121+
MapstructUtil.MAPPER_ANNOTATION_FQN
122+
);
123+
124+
if ( mapperAnnotation == null ) {
125+
return Stream.empty();
126+
}
127+
128+
return findReferencedMapperClasses( mapperAnnotation )
129+
.flatMap( psiClass -> Arrays.stream( psiClass.getMethods() ) )
130+
.filter( MapstructUtil::isNamedMethod );
131+
}
132+
133+
private LookupElement methodAsLookup(@NotNull PsiMethod method) {
134+
String lookupString = getNamedValue( method );
135+
if ( StringUtil.isEmpty( lookupString ) ) {
136+
return null;
137+
}
138+
139+
return asLookupWithRepresentableText(
140+
method,
141+
lookupString,
142+
lookupString,
143+
String.format(
144+
" %s#%s(%s)",
145+
Objects.requireNonNull( method.getContainingClass() ).getName(),
146+
method.getName(),
147+
formatParameters( method )
148+
)
149+
);
150+
}
151+
152+
@NotNull
153+
private static String formatParameters(@NotNull PsiMethod method) {
154+
return Arrays.stream( method.getParameterList().getParameters() )
155+
.map( PsiParameter::getType )
156+
.map( PsiType::getPresentableText )
157+
.collect( Collectors.joining( ", " ) );
158+
}
159+
160+
@Nullable
161+
@Override
162+
PsiType resolvedType() {
163+
return null;
164+
}
165+
166+
/**
167+
* @param psiElement the literal for which references need to be created
168+
* @return the references for the given {@code psiLiteral}
169+
*/
170+
static PsiReference[] create(PsiElement psiElement) {
171+
return MapstructBaseReference.create( psiElement, MapstructMappingQualifiedByNameReference::new, false );
172+
}
173+
174+
}

src/main/java/org/mapstruct/intellij/codeinsight/references/MapstructReferenceContributor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar)
2626
mappingElementPattern( "target" ),
2727
new MappingTargetReferenceProvider( MapstructTargetReference::create )
2828
);
29+
registrar.registerReferenceProvider(
30+
mappingElementPattern( "qualifiedByName" ),
31+
new MappingTargetReferenceProvider( MapstructMappingQualifiedByNameReference::create )
32+
);
2933
registrar.registerReferenceProvider(
3034
mappingElementPattern( "source" ),
3135
new MappingTargetReferenceProvider( MapstructSourceReference::create )

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

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

8+
import java.util.ArrayList;
89
import java.util.Collections;
10+
import java.util.List;
911
import java.util.Objects;
1012
import java.util.Optional;
13+
import java.util.stream.Collectors;
1114
import java.util.stream.Stream;
1215

1316
import com.intellij.codeInsight.AnnotationUtil;
@@ -31,13 +34,15 @@
3134
import com.intellij.psi.PsiAnnotation;
3235
import com.intellij.psi.PsiAnnotationMemberValue;
3336
import com.intellij.psi.PsiArrayInitializerMemberValue;
37+
import com.intellij.psi.PsiClass;
3438
import com.intellij.psi.PsiClassObjectAccessExpression;
3539
import com.intellij.psi.PsiElement;
3640
import com.intellij.psi.PsiFile;
3741
import com.intellij.psi.PsiJavaCodeReferenceElement;
3842
import com.intellij.psi.PsiMethod;
3943
import com.intellij.psi.PsiModifierListOwner;
4044
import com.intellij.psi.PsiNameValuePair;
45+
import com.intellij.psi.PsiReference;
4146
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
4247
import com.intellij.util.IncorrectOperationException;
4348
import org.jetbrains.annotations.NotNull;
@@ -407,4 +412,71 @@ public static PsiModifierListOwner findMapperConfigReference(PsiAnnotation mappe
407412

408413
return (PsiModifierListOwner) resolvedElement;
409414
}
415+
416+
/**
417+
* Find the other mapper types used by the class or interface defined in the {@code mapperAnnotation}
418+
*
419+
* @param mapperAnnotation the mapper annotation in which the mapper config is defined
420+
* @return the classes / interfaces that are defined with the {@code uses} attribute of the current
421+
* {@code mapperAnnotation} or referenced @MappingConfig, or and empty stream if there isn't anything defined
422+
*/
423+
public static Stream<PsiClass> findReferencedMapperClasses(PsiAnnotation mapperAnnotation) {
424+
425+
Stream<PsiClass> localUsesReferences = findReferencedMappers( mapperAnnotation );
426+
427+
Stream<PsiClass> mapperConfigUsesReferences = findReferencedMappersOfMapperConfig( mapperAnnotation );
428+
429+
return Stream.concat( localUsesReferences, mapperConfigUsesReferences );
430+
}
431+
432+
@NotNull
433+
private static Stream<PsiClass> findReferencedMappers(PsiAnnotation mapperAnnotation) {
434+
PsiNameValuePair usesAttribute = findDeclaredAttribute( mapperAnnotation, "uses" );
435+
if ( usesAttribute == null ) {
436+
return Stream.empty();
437+
}
438+
439+
PsiAnnotationMemberValue usesValue = usesAttribute.getValue();
440+
441+
List<PsiClassObjectAccessExpression> usesExpressions = new ArrayList<>();
442+
if ( usesValue instanceof PsiArrayInitializerMemberValue ) {
443+
usesExpressions = Stream.of( ( (PsiArrayInitializerMemberValue) usesValue )
444+
.getInitializers() )
445+
.filter( PsiClassObjectAccessExpression.class::isInstance )
446+
.map( PsiClassObjectAccessExpression.class::cast )
447+
.collect( Collectors.toList() );
448+
}
449+
else if ( usesValue instanceof PsiClassObjectAccessExpression ) {
450+
usesExpressions = List.of( (PsiClassObjectAccessExpression) usesValue );
451+
}
452+
453+
return usesExpressions.stream()
454+
.map( usesExpression -> usesExpression.getOperand().getInnermostComponentReferenceElement() )
455+
.filter( Objects::nonNull )
456+
.map( PsiReference::resolve )
457+
.filter( PsiClass.class::isInstance )
458+
.map( PsiClass.class::cast );
459+
}
460+
461+
private static Stream<PsiClass> findReferencedMappersOfMapperConfig(PsiAnnotation mapperAnnotation) {
462+
463+
PsiModifierListOwner mapperConfigReference = findMapperConfigReference( mapperAnnotation );
464+
465+
if ( mapperConfigReference == null ) {
466+
return Stream.empty();
467+
}
468+
469+
PsiAnnotation mapperConfigAnnotation = findAnnotation(
470+
mapperConfigReference,
471+
true,
472+
MapstructUtil.MAPPER_CONFIG_ANNOTATION_FQN
473+
);
474+
475+
if ( mapperConfigAnnotation == null ) {
476+
return Stream.empty();
477+
}
478+
479+
return findReferencedMappers( mapperConfigAnnotation );
480+
}
481+
410482
}

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import org.mapstruct.Mapping;
5656
import org.mapstruct.MappingTarget;
5757
import org.mapstruct.Mappings;
58+
import org.mapstruct.Named;
5859
import org.mapstruct.ValueMapping;
5960
import org.mapstruct.ValueMappings;
6061
import org.mapstruct.factory.Mappers;
@@ -83,6 +84,8 @@ public final class MapstructUtil {
8384

8485
public static final String BEAN_MAPPING_FQN = BeanMapping.class.getName();
8586

87+
public static final String NAMED_ANNOTATION_FQN = Named.class.getName();
88+
8689
static final String MAPPINGS_ANNOTATION_FQN = Mappings.class.getName();
8790
static final String VALUE_MAPPING_ANNOTATION_FQN = ValueMapping.class.getName();
8891
static final String VALUE_MAPPINGS_ANNOTATION_FQN = ValueMappings.class.getName();
@@ -131,6 +134,21 @@ public static LookupElement asLookup(PsiEnumConstant enumConstant) {
131134
return asLookup( enumConstant.getName(), enumConstant, PsiField::getType, PlatformIcons.FIELD_ICON );
132135
}
133136

137+
public static LookupElement asLookupWithRepresentableText(PsiMethod method, String lookupString,
138+
String representableText, String tailText) {
139+
LookupElementBuilder builder = LookupElementBuilder.create( method, lookupString )
140+
.withIcon( PlatformIcons.METHOD_ICON )
141+
.withPresentableText( representableText )
142+
.withTailText( tailText );
143+
144+
final PsiType type = method.getReturnType();
145+
if ( type != null ) {
146+
builder = builder.withTypeText( EmptySubstitutor.getInstance().substitute( type ).getPresentableText() );
147+
}
148+
149+
return builder;
150+
}
151+
134152
public static <T extends PsiElement> LookupElement asLookup(String propertyName, @NotNull T psiElement,
135153
Function<T, PsiType> typeMapper, Icon icon) {
136154
//noinspection unchecked
@@ -319,6 +337,16 @@ public static boolean isMappingMethod(PsiMethod psiMethod) {
319337
|| isAnnotated( psiMethod, VALUE_MAPPINGS_ANNOTATION_FQN, AnnotationUtil.CHECK_TYPE );
320338
}
321339

340+
/**
341+
* Checks if the method is annotated with {@code Named}.
342+
*
343+
* @param psiMethod to be checked
344+
* @return {@code true} if the method is annotated with {@code Named}, {@code false} otherwise
345+
*/
346+
public static boolean isNamedMethod(PsiMethod psiMethod) {
347+
return isAnnotated( psiMethod, NAMED_ANNOTATION_FQN, AnnotationUtil.CHECK_TYPE );
348+
}
349+
322350
/**
323351
* Checks if the parameter is a valid source parameter. A valid source parameter is a paremeter that is not a
324352
* {@code MappingTarget} or a {@code Context}.

0 commit comments

Comments
 (0)