Skip to content

Commit

Permalink
Suggest only missing constants for ValueMapping source
Browse files Browse the repository at this point in the history
closes #5
closes #103
  • Loading branch information
hduelme authored and filiphr committed Nov 20, 2022
1 parent 48b5188 commit 2a80757
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 3 deletions.
1 change: 1 addition & 0 deletions change-notes.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ <h2>1.4.0</h2>
<li>Suppress redundant default parameter value assignment warning for <code>Mapping#constant</code> and <code>Mapping#defaultValue</code></li>
<li>Support for Java records</li>
<li>Support MapStruct explicit <code>Builder#disableBuilder</code> through <code>@MapperConfig</code></li>
<li><code>@ValueMapping</code> source code completion should only suggest unmapped source constants</li>
<li>Bug fix: language injections inside expressions when target is field</li>
</ul>
<h2>1.3.1</h2>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
*/
package org.mapstruct.intellij.codeinsight.references;

import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.intellij.codeInsight.lookup.LookupElement;
Expand All @@ -16,8 +18,9 @@
import com.intellij.psi.PsiReference;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mapstruct.intellij.util.MapstructUtil;
import org.mapstruct.intellij.util.ValueMappingUtils;

import static org.mapstruct.intellij.util.MapstructUtil.asLookup;
import static org.mapstruct.intellij.util.SourceUtils.getParameterClass;

/**
Expand Down Expand Up @@ -59,9 +62,14 @@ Object[] getVariantsInternal(@NotNull PsiMethod mappingMethod) {
return LookupElement.EMPTY_ARRAY;
}

Set<String> alreadyDefinedValues = ValueMappingUtils.findAllDefinedValueMappingSources( mappingMethod )
.collect( Collectors.toSet() );

return Stream.of( sourceClass.getFields() )
.filter( psiField -> psiField instanceof PsiEnumConstant )
.map( psiEnumConstant -> asLookup( (PsiEnumConstant) psiEnumConstant ) )
.filter( PsiEnumConstant.class::isInstance )
.map( PsiEnumConstant.class::cast )
.filter( enumConstant -> !alreadyDefinedValues.contains( enumConstant.getName() ) )
.map( MapstructUtil::asLookup )
.toArray( LookupElement[]::new );
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import static com.intellij.codeInsight.intention.AddAnnotationPsiFix.addPhysicalAnnotationTo;
import static com.intellij.codeInsight.intention.AddAnnotationPsiFix.removePhysicalAnnotations;
import static org.mapstruct.intellij.util.MapstructUtil.MAPPING_ANNOTATION_FQN;
import static org.mapstruct.intellij.util.MapstructUtil.VALUE_MAPPING_ANNOTATION_FQN;

/**
* Utils for working with mapstruct annotation.
Expand Down Expand Up @@ -269,6 +270,32 @@ private static Stream<PsiAnnotation> findMappingAnnotations(@NotNull PsiMethod m
.filter( MapstructAnnotationUtils::isMappingAnnotation );
}

public static Stream<PsiAnnotation> findAllDefinedValueMappingAnnotations(@NotNull PsiMethod method) {
Stream<PsiAnnotation> valueMappingsAnnotations = Stream.empty();
PsiAnnotation valueMappings = findAnnotation( method, true, MapstructUtil.VALUE_MAPPINGS_ANNOTATION_FQN );
if ( valueMappings != null ) {
PsiNameValuePair mappingsValue = findDeclaredAttribute( valueMappings, null );
if ( mappingsValue != null && mappingsValue.getValue() instanceof PsiArrayInitializerMemberValue ) {
valueMappingsAnnotations = Stream.of( ( (PsiArrayInitializerMemberValue) mappingsValue.getValue() )
.getInitializers() )
.filter( MapstructAnnotationUtils::isValueMappingPsiAnnotation )
.map( memberValue -> (PsiAnnotation) memberValue );
}
else if ( mappingsValue != null && mappingsValue.getValue() instanceof PsiAnnotation ) {
valueMappingsAnnotations = Stream.of( (PsiAnnotation) mappingsValue.getValue() );
}
}

Stream<PsiAnnotation> valueMappingAnnotations = findValueMappingAnnotations( method );

return Stream.concat( valueMappingAnnotations, valueMappingsAnnotations );
}

private static Stream<PsiAnnotation> findValueMappingAnnotations(@NotNull PsiMethod method) {
return Stream.of( method.getModifierList().getAnnotations() )
.filter( MapstructAnnotationUtils::isValueMappingAnnotation );
}

/**
* @param memberValue that needs to be checked
*
Expand All @@ -280,6 +307,27 @@ private static boolean isMappingPsiAnnotation(PsiAnnotationMemberValue memberVal
&& isMappingAnnotation( (PsiAnnotation) memberValue );
}

/**
* @param memberValue that needs to be checked
*
* @return {@code true} if the {@code memberValue} is the {@link org.mapstruct.ValueMapping} {@link PsiAnnotation},
* {@code false} otherwise
*/
private static boolean isValueMappingPsiAnnotation(PsiAnnotationMemberValue memberValue) {
return memberValue instanceof PsiAnnotation
&& isValueMappingAnnotation( (PsiAnnotation) memberValue );
}

/**
* @param psiAnnotation that needs to be checked
*
* @return {@code true} if the {@code psiAnnotation} is the {@link org.mapstruct.ValueMapping} annotation,
* {@code false} otherwise
*/
private static boolean isValueMappingAnnotation(PsiAnnotation psiAnnotation) {
return Objects.equals( psiAnnotation.getQualifiedName(), VALUE_MAPPING_ANNOTATION_FQN );
}

/**
* @param psiAnnotation that needs to be checked
*
Expand Down
38 changes: 38 additions & 0 deletions src/main/java/org/mapstruct/intellij/util/ValueMappingUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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.util;

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

import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.psi.PsiMethod;
import org.jetbrains.annotations.NotNull;

import static org.mapstruct.intellij.util.MapstructAnnotationUtils.findAllDefinedValueMappingAnnotations;

/**
* @author Filip Hrisafov
*/
public class ValueMappingUtils {

private ValueMappingUtils() {
}

/**
* Find all defined {@link org.mapstruct.ValueMapping#source()} for the given method
*
* @param method that needs to be checked
*
* @return see description
*/
public static Stream<String> findAllDefinedValueMappingSources(@NotNull PsiMethod method) {
return findAllDefinedValueMappingAnnotations( method )
.map( psiAnnotation -> AnnotationUtil.getDeclaredStringAttributeValue( psiAnnotation, "source" ) )
.filter( Objects::nonNull )
.filter( s -> !s.isEmpty() );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,33 @@
*/
public class ValueMappingCompletionTestCase extends MapstructBaseCompletionTestCase {

@Language("JAVA")
private static final String SOURCE_VALUE_MAPPING_DYNAMIC = "import org.mapstruct.Mapper;\n" +
"import org.mapstruct.ValueMapping;\n" +
"import org.mapstruct.example.ExternalRoofType;\n" +
"import org.mapstruct.example.RoofType;\n" +
"\n" +
"@Mapper\n" +
"public interface RoofTypeMapper {\n" +
"\n" +
" %s" +
" ExternalRoofType map(RoofType type);\n" +
"}";

@Language("JAVA")
private static final String SOURCE_VALUE_MAPPINGS_DYNAMIC = "import org.mapstruct.Mapper;\n" +
"import org.mapstruct.ValueMapping;\n" +
"import org.mapstruct.ValueMappings;\n" +
"import org.mapstruct.example.ExternalRoofType;\n" +
"import org.mapstruct.example.RoofType;\n" +
"\n" +
"@Mapper\n" +
"public interface RoofTypeMapper {\n" +
"\n" +
" @ValueMappings({\n%s\n})\n" +
" ExternalRoofType map(RoofType type);\n" +
"}";

@Language("JAVA")
private static final String SOURCE_VALUE_MAPPING = "import org.mapstruct.Mapper;\n" +
"import org.mapstruct.ValueMapping;\n" +
Expand Down Expand Up @@ -82,6 +109,100 @@ public void testSourceValueMappingVariants() {
);
}

public void testSourceValueMappingWithExisting() {
String source = String.format(
SOURCE_VALUE_MAPPING_DYNAMIC,
"@ValueMapping(source = \"GAMBREL\", target = \"NORMAL\")\n" +
"@ValueMapping(source = \"<caret>%s\", target = \"STANDARD\")\n"
);
myFixture.configureByText( JavaFileType.INSTANCE, source );
complete();

assertThat( myItems )
.extracting( LookupElement::getLookupString )
.containsExactlyInAnyOrder(
"OPEN",
"BOX",
"NORMAL"
);
assertThat( myItems )
.extracting( LookupElementPresentation::renderElement )
.usingElementComparatorIgnoringFields( "myIcon" )
.containsExactlyInAnyOrder(
createField( "OPEN", "RoofType" ),
createField( "BOX", "RoofType" ),
createField( "NORMAL", "RoofType" )
);
}

public void testSourceValueMappingsWithExisting() {
String source = String.format(
SOURCE_VALUE_MAPPINGS_DYNAMIC,
"@ValueMapping(source = \"GAMBREL\", target = \"NORMAL\"),\n" +
"@ValueMapping(source = \"<caret>%s\", target = \"STANDARD\")\n"
);
myFixture.configureByText( JavaFileType.INSTANCE, source );
complete();

assertThat( myItems )
.extracting( LookupElement::getLookupString )
.containsExactlyInAnyOrder(
"OPEN",
"BOX",
"NORMAL"
);
assertThat( myItems )
.extracting( LookupElementPresentation::renderElement )
.usingElementComparatorIgnoringFields( "myIcon" )
.containsExactlyInAnyOrder(
createField( "OPEN", "RoofType" ),
createField( "BOX", "RoofType" ),
createField( "NORMAL", "RoofType" )
);
}

public void testSourceValueMappingAllValuesAlreadyMapped() {
String source = String.format(
SOURCE_VALUE_MAPPING_DYNAMIC,
"@ValueMapping(source = \"OPEN\", target = \"NORMAL\")\n" +
"@ValueMapping(source = \"BOX\", target = \"NORMAL\")\n" +
"@ValueMapping(source = \"GAMBREL\", target = \"NORMAL\")\n" +
"@ValueMapping(source = \"NORMAL\", target = \"NORMAL\")\n" +
"@ValueMapping(source = \"<caret>%s\", target = \"STANDARD\")\n"
);
myFixture.configureByText( JavaFileType.INSTANCE, source );
complete();

assertThat( myItems )
.extracting( LookupElement::getLookupString )
.isEmpty();
assertThat( myItems )
.extracting( LookupElementPresentation::renderElement )
.usingElementComparatorIgnoringFields( "myIcon" )
.isEmpty();
}

public void testSourceValueMappingsAllValuesAlreadyMapped() {
String source = String.format(
SOURCE_VALUE_MAPPINGS_DYNAMIC,
"@ValueMapping(source = \"OPEN\", target = \"NORMAL\"),\n" +
"@ValueMapping(source = \"BOX\", target = \"NORMAL\"),\n" +
"@ValueMapping(source = \"GAMBREL\", target = \"NORMAL\"),\n" +
"@ValueMapping(source = \"NORMAL\", target = \"NORMAL\"),\n" +
"@ValueMapping(source = \"<caret>%s\", target = \"STANDARD\")\n"
);
myFixture.configureByText( JavaFileType.INSTANCE, source );
complete();

assertThat( myItems )
.extracting( LookupElement::getLookupString )
.isEmpty();
assertThat( myItems )
.extracting( LookupElementPresentation::renderElement )
.usingElementComparatorIgnoringFields( "myIcon" )
.isEmpty();
}

public void testSourceValueMappingResolveToEnum() {
myFixture.configureByText( JavaFileType.INSTANCE, String.format( SOURCE_VALUE_MAPPING, "NORMAL" ) );

Expand Down

0 comments on commit 2a80757

Please sign in to comment.