Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that for this it would be good to show the method parameters here as well. e.g. StringMapper#trim(String).

" %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 @@ -24,13 +27,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 @@ -370,4 +375,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