Skip to content

Commit

Permalink
#107 Support go to declaration and completion for `Mapping#qualifiedB…
Browse files Browse the repository at this point in the history
…yName`
  • Loading branch information
thunderhook authored Apr 2, 2023
1 parent 243f7b8 commit 3047d18
Show file tree
Hide file tree
Showing 10 changed files with 475 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.intellij.codeinsight.references;

import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mapstruct.intellij.util.MapstructUtil;

import static com.intellij.codeInsight.AnnotationUtil.findAnnotation;
import static com.intellij.codeInsight.AnnotationUtil.getStringAttributeValue;
import static org.mapstruct.intellij.util.MapstructAnnotationUtils.findReferencedMapperClasses;
import static org.mapstruct.intellij.util.MapstructUtil.asLookupWithRepresentableText;

/**
* Reference for {@link org.mapstruct.Mapping#qualifiedByName()}.
*
* @author Oliver Erhart
*/
class MapstructMappingQualifiedByNameReference extends MapstructBaseReference {

/**
* Create a new {@link MapstructMappingQualifiedByNameReference} with the provided parameters
*
* @param element the element that the reference belongs to
* @param previousReference the previous reference if there is one (in nested properties for example)
* @param rangeInElement the range that the reference represent in the {@code element}
* @param value the matched value (useful when {@code rangeInElement} is empty)
*/
private MapstructMappingQualifiedByNameReference(PsiElement element,
MapstructMappingQualifiedByNameReference previousReference,
TextRange rangeInElement, String value) {
super( element, previousReference, rangeInElement, value );
}

@Override
PsiElement resolveInternal(@NotNull String value, @NotNull PsiType psiType) {
return null; // not needed
}

@Override
PsiElement resolveInternal(@NotNull String value, @NotNull PsiMethod mappingMethod) {

return findAllNamedMethodsFromThisAndReferencedMappers( mappingMethod )
.filter( a -> Objects.equals( getNamedValue( a ), value ) )
.findAny()
.orElse( null );
}

@Nullable
private String getNamedValue(PsiMethod method) {

PsiAnnotation annotation = findAnnotation( method, true, MapstructUtil.NAMED_ANNOTATION_FQN );

if ( annotation == null ) {
return null;
}

return getStringAttributeValue( annotation, "value" );
}

@NotNull
@Override
Object[] getVariantsInternal(@NotNull PsiType psiType) {
return LookupElement.EMPTY_ARRAY; // not needed
}

@NotNull
@Override
Object[] getVariantsInternal(@NotNull PsiMethod mappingMethod) {

return findAllNamedMethodsFromThisAndReferencedMappers( mappingMethod )
.map( this::methodAsLookup )
.filter( Objects::nonNull )
.toArray();
}

private boolean methodHasReturnType(@NotNull PsiMethod psiMethod) {
return !PsiType.VOID.equals( psiMethod.getReturnType() );
}

@NotNull
private Stream<PsiMethod> findAllNamedMethodsFromThisAndReferencedMappers(@NotNull PsiMethod mappingMethod) {

PsiClass containingClass = mappingMethod.getContainingClass();
if ( containingClass == null ) {
return Stream.empty();
}

Stream<PsiMethod> internalMethods = Stream.of( containingClass.getMethods() )
.filter( MapstructUtil::isNamedMethod );

Stream<PsiMethod> externalMethods = findNamedMethodsInUsedMappers( containingClass );

return Stream.concat( internalMethods, externalMethods )
.filter( this::methodHasReturnType );
}

@NotNull
private Stream<PsiMethod> findNamedMethodsInUsedMappers(@Nullable PsiClass containingClass) {

PsiAnnotation mapperAnnotation = findAnnotation(
containingClass,
MapstructUtil.MAPPER_ANNOTATION_FQN
);

if ( mapperAnnotation == null ) {
return Stream.empty();
}

return findReferencedMapperClasses( mapperAnnotation )
.flatMap( psiClass -> Arrays.stream( psiClass.getMethods() ) )
.filter( MapstructUtil::isNamedMethod );
}

private LookupElement methodAsLookup(@NotNull PsiMethod method) {
String lookupString = getNamedValue( method );
if ( StringUtil.isEmpty( lookupString ) ) {
return null;
}

return asLookupWithRepresentableText(
method,
lookupString,
lookupString,
String.format(
" %s#%s(%s)",
Objects.requireNonNull( method.getContainingClass() ).getName(),
method.getName(),
formatParameters( method )
)
);
}

@NotNull
private static String formatParameters(@NotNull PsiMethod method) {
return Arrays.stream( method.getParameterList().getParameters() )
.map( PsiParameter::getType )
.map( PsiType::getPresentableText )
.collect( Collectors.joining( ", " ) );
}

@Nullable
@Override
PsiType resolvedType() {
return null;
}

/**
* @param psiElement the literal for which references need to be created
* @return the references for the given {@code psiLiteral}
*/
static PsiReference[] create(PsiElement psiElement) {
return MapstructBaseReference.create( psiElement, MapstructMappingQualifiedByNameReference::new, false );
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar)
mappingElementPattern( "target" ),
new MappingTargetReferenceProvider( MapstructTargetReference::create )
);
registrar.registerReferenceProvider(
mappingElementPattern( "qualifiedByName" ),
new MappingTargetReferenceProvider( MapstructMappingQualifiedByNameReference::create )
);
registrar.registerReferenceProvider(
mappingElementPattern( "source" ),
new MappingTargetReferenceProvider( MapstructSourceReference::create )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
*/
package org.mapstruct.intellij.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.intellij.codeInsight.AnnotationUtil;
Expand All @@ -31,13 +34,15 @@
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnnotationMemberValue;
import com.intellij.psi.PsiArrayInitializerMemberValue;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassObjectAccessExpression;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiJavaCodeReferenceElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.PsiNameValuePair;
import com.intellij.psi.PsiReference;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.NotNull;
Expand Down Expand Up @@ -407,4 +412,71 @@ public static PsiModifierListOwner findMapperConfigReference(PsiAnnotation mappe

return (PsiModifierListOwner) resolvedElement;
}

/**
* Find the other mapper types used by the class or interface defined in the {@code mapperAnnotation}
*
* @param mapperAnnotation the mapper annotation in which the mapper config is defined
* @return the classes / interfaces that are defined with the {@code uses} attribute of the current
* {@code mapperAnnotation} or referenced @MappingConfig, or and empty stream if there isn't anything defined
*/
public static Stream<PsiClass> findReferencedMapperClasses(PsiAnnotation mapperAnnotation) {

Stream<PsiClass> localUsesReferences = findReferencedMappers( mapperAnnotation );

Stream<PsiClass> mapperConfigUsesReferences = findReferencedMappersOfMapperConfig( mapperAnnotation );

return Stream.concat( localUsesReferences, mapperConfigUsesReferences );
}

@NotNull
private static Stream<PsiClass> findReferencedMappers(PsiAnnotation mapperAnnotation) {
PsiNameValuePair usesAttribute = findDeclaredAttribute( mapperAnnotation, "uses" );
if ( usesAttribute == null ) {
return Stream.empty();
}

PsiAnnotationMemberValue usesValue = usesAttribute.getValue();

List<PsiClassObjectAccessExpression> usesExpressions = new ArrayList<>();
if ( usesValue instanceof PsiArrayInitializerMemberValue ) {
usesExpressions = Stream.of( ( (PsiArrayInitializerMemberValue) usesValue )
.getInitializers() )
.filter( PsiClassObjectAccessExpression.class::isInstance )
.map( PsiClassObjectAccessExpression.class::cast )
.collect( Collectors.toList() );
}
else if ( usesValue instanceof PsiClassObjectAccessExpression ) {
usesExpressions = List.of( (PsiClassObjectAccessExpression) usesValue );
}

return usesExpressions.stream()
.map( usesExpression -> usesExpression.getOperand().getInnermostComponentReferenceElement() )
.filter( Objects::nonNull )
.map( PsiReference::resolve )
.filter( PsiClass.class::isInstance )
.map( PsiClass.class::cast );
}

private static Stream<PsiClass> findReferencedMappersOfMapperConfig(PsiAnnotation mapperAnnotation) {

PsiModifierListOwner mapperConfigReference = findMapperConfigReference( mapperAnnotation );

if ( mapperConfigReference == null ) {
return Stream.empty();
}

PsiAnnotation mapperConfigAnnotation = findAnnotation(
mapperConfigReference,
true,
MapstructUtil.MAPPER_CONFIG_ANNOTATION_FQN
);

if ( mapperConfigAnnotation == null ) {
return Stream.empty();
}

return findReferencedMappers( mapperConfigAnnotation );
}

}
28 changes: 28 additions & 0 deletions src/main/java/org/mapstruct/intellij/util/MapstructUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.Mappings;
import org.mapstruct.Named;
import org.mapstruct.ValueMapping;
import org.mapstruct.ValueMappings;
import org.mapstruct.factory.Mappers;
Expand Down Expand Up @@ -83,6 +84,8 @@ public final class MapstructUtil {

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

public static final String NAMED_ANNOTATION_FQN = Named.class.getName();

static final String MAPPINGS_ANNOTATION_FQN = Mappings.class.getName();
static final String VALUE_MAPPING_ANNOTATION_FQN = ValueMapping.class.getName();
static final String VALUE_MAPPINGS_ANNOTATION_FQN = ValueMappings.class.getName();
Expand Down Expand Up @@ -131,6 +134,21 @@ public static LookupElement asLookup(PsiEnumConstant enumConstant) {
return asLookup( enumConstant.getName(), enumConstant, PsiField::getType, PlatformIcons.FIELD_ICON );
}

public static LookupElement asLookupWithRepresentableText(PsiMethod method, String lookupString,
String representableText, String tailText) {
LookupElementBuilder builder = LookupElementBuilder.create( method, lookupString )
.withIcon( PlatformIcons.METHOD_ICON )
.withPresentableText( representableText )
.withTailText( tailText );

final PsiType type = method.getReturnType();
if ( type != null ) {
builder = builder.withTypeText( EmptySubstitutor.getInstance().substitute( type ).getPresentableText() );
}

return builder;
}

public static <T extends PsiElement> LookupElement asLookup(String propertyName, @NotNull T psiElement,
Function<T, PsiType> typeMapper, Icon icon) {
//noinspection unchecked
Expand Down Expand Up @@ -319,6 +337,16 @@ public static boolean isMappingMethod(PsiMethod psiMethod) {
|| isAnnotated( psiMethod, VALUE_MAPPINGS_ANNOTATION_FQN, AnnotationUtil.CHECK_TYPE );
}

/**
* Checks if the method is annotated with {@code Named}.
*
* @param psiMethod to be checked
* @return {@code true} if the method is annotated with {@code Named}, {@code false} otherwise
*/
public static boolean isNamedMethod(PsiMethod psiMethod) {
return isAnnotated( psiMethod, NAMED_ANNOTATION_FQN, AnnotationUtil.CHECK_TYPE );
}

/**
* Checks if the parameter is a valid source parameter. A valid source parameter is a paremeter that is not a
* {@code MappingTarget} or a {@code Context}.
Expand Down
Loading

0 comments on commit 3047d18

Please sign in to comment.