Skip to content

Commit 95e03b3

Browse files
committed
Support for MapStruct explicit builder disabling
Closes #67
1 parent b2c649b commit 95e03b3

13 files changed

+494
-10
lines changed

change-notes.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<h2>1.3.0</h2>
33
<ul>
44
<li>Support code completion in <code>BeanMapping#ignoreUnmappedSourceProperties</code></li>
5+
<li>Support MapStruct explicit <code>Builder#disableBuilder</code> through <code>@BeanMapping</code> or <code>@Mapper</code></li>
56
<li>Quick Fix: support for configuring the order of source and target in <code>@Mapping</code> for "Add unmapped property" fix</li>
67
<li>Bug fix: Code completion for generic builder</li>
78
<li>Bug fix: Code completion uses build constructor parameters</li>

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import static org.mapstruct.intellij.util.MapstructUtil.isPublicModifiable;
3232
import static org.mapstruct.intellij.util.MapstructUtil.isPublicNonStatic;
3333
import static org.mapstruct.intellij.util.TargetUtils.getRelevantType;
34+
import static org.mapstruct.intellij.util.TargetUtils.isBuilderEnabled;
3435
import static org.mapstruct.intellij.util.TargetUtils.publicWriteAccessors;
3536
import static org.mapstruct.intellij.util.TargetUtils.resolveBuilderOrSelfClass;
3637

@@ -60,7 +61,10 @@ private MapstructTargetReference(PsiElement element, MapstructTargetReference pr
6061
@Override
6162
PsiElement resolveInternal(@NotNull String value, @NotNull PsiType psiType) {
6263
boolean builderSupportPresent = mapStructVersion.isBuilderSupported();
63-
Pair<PsiClass, TargetType> pair = resolveBuilderOrSelfClass( psiType, builderSupportPresent );
64+
Pair<PsiClass, TargetType> pair = resolveBuilderOrSelfClass(
65+
psiType,
66+
builderSupportPresent && isBuilderEnabled( getMappingMethod() )
67+
);
6468
if ( pair == null ) {
6569
return null;
6670
}
@@ -128,7 +132,7 @@ PsiElement resolveInternal(@NotNull String value, @NotNull PsiMethod mappingMeth
128132
@Override
129133
Object[] getVariantsInternal(@NotNull PsiType psiType) {
130134
return asLookup(
131-
publicWriteAccessors( psiType, mapStructVersion ),
135+
publicWriteAccessors( psiType, mapStructVersion, getMappingMethod() ),
132136
MapstructTargetReference::memberPsiType
133137
);
134138
}

src/main/java/org/mapstruct/intellij/inspection/UnmappedTargetPropertiesInspection.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public void visitMethod(PsiMethod method) {
8282
return;
8383
}
8484

85-
Set<String> allTargetProperties = findAllTargetProperties( targetType, mapStructVersion );
85+
Set<String> allTargetProperties = findAllTargetProperties( targetType, mapStructVersion, method );
8686

8787
// find and remove all defined mapping targets
8888
Set<String> definedTargets = TargetUtils.findAllDefinedMappingTargets( method, mapStructVersion )

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

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,21 @@
1515
import java.util.List;
1616
import java.util.Map;
1717
import java.util.Objects;
18+
import java.util.Optional;
1819
import java.util.Set;
1920
import java.util.stream.Stream;
2021

2122
import com.intellij.codeInsight.AnnotationUtil;
2223
import com.intellij.lang.jvm.JvmModifier;
2324
import com.intellij.openapi.util.Pair;
2425
import com.intellij.psi.PsiAnnotation;
26+
import com.intellij.psi.PsiAnnotationMemberValue;
2527
import com.intellij.psi.PsiClass;
2628
import com.intellij.psi.PsiElement;
2729
import com.intellij.psi.PsiJavaCodeReferenceElement;
2830
import com.intellij.psi.PsiMember;
2931
import com.intellij.psi.PsiMethod;
32+
import com.intellij.psi.PsiModifierListOwner;
3033
import com.intellij.psi.PsiNameValuePair;
3134
import com.intellij.psi.PsiParameter;
3235
import com.intellij.psi.PsiSubstitutor;
@@ -36,6 +39,8 @@
3639
import org.jetbrains.annotations.NotNull;
3740
import org.jetbrains.annotations.Nullable;
3841

42+
import static com.intellij.codeInsight.AnnotationUtil.findAnnotation;
43+
import static com.intellij.codeInsight.AnnotationUtil.getBooleanAttributeValue;
3944
import static org.mapstruct.intellij.util.MapstructAnnotationUtils.findAllDefinedMappingAnnotations;
4045
import static org.mapstruct.intellij.util.MapstructUtil.canDescendIntoType;
4146
import static org.mapstruct.intellij.util.MapstructUtil.isFluentSetter;
@@ -84,13 +89,17 @@ public static PsiType getRelevantType(@NotNull PsiMethod mappingMethod) {
8489
*
8590
* @param psiType to use to extract the accessors
8691
* @param mapStructVersion the MapStruct project version
92+
* @param mappingMethod the mapping method
8793
*
8894
* @return a stream that holds all public write accessors for the given {@code psiType}
8995
*/
9096
public static Map<String, Pair<? extends PsiElement, PsiSubstitutor>> publicWriteAccessors(@NotNull PsiType psiType,
91-
MapStructVersion mapStructVersion) {
97+
MapStructVersion mapStructVersion, PsiMethod mappingMethod) {
9298
boolean builderSupportPresent = mapStructVersion.isBuilderSupported();
93-
Pair<PsiClass, TargetType> classAndType = resolveBuilderOrSelfClass( psiType, builderSupportPresent );
99+
Pair<PsiClass, TargetType> classAndType = resolveBuilderOrSelfClass(
100+
psiType,
101+
builderSupportPresent && isBuilderEnabled( mappingMethod )
102+
);
94103
if ( classAndType == null ) {
95104
return Collections.emptyMap();
96105
}
@@ -111,6 +120,60 @@ public static Map<String, Pair<? extends PsiElement, PsiSubstitutor>> publicWrit
111120
return publicWriteAccessors;
112121
}
113122

123+
/**
124+
* Whether builder is enabled for the mapping method with the mapstruct version
125+
*
126+
* @param mapStructVersion the MapStruct project version
127+
* @param mappingMethod the mapping method
128+
*
129+
* @return {@code true} if builder can be used for the mapping method
130+
*/
131+
132+
public static boolean isBuilderEnabled(MapStructVersion mapStructVersion, PsiMethod mappingMethod) {
133+
if ( mapStructVersion.isBuilderSupported() ) {
134+
return isBuilderEnabled( mappingMethod );
135+
}
136+
137+
return false;
138+
}
139+
140+
/**
141+
* Whether builder is enabled for the mapping method
142+
*
143+
* @param mappingMethod the mapping method
144+
*
145+
* @return {@code true} if builder can be used for the mapping method
146+
*/
147+
public static boolean isBuilderEnabled(@Nullable PsiMethod mappingMethod) {
148+
Optional<Boolean> disableBuilder = findDisableBuilder( mappingMethod, MapstructUtil.BEAN_MAPPING_FQN );
149+
150+
if ( !disableBuilder.isPresent() && mappingMethod != null ) {
151+
disableBuilder = findDisableBuilder(
152+
mappingMethod.getContainingClass(),
153+
MapstructUtil.MAPPER_ANNOTATION_FQN
154+
);
155+
}
156+
157+
return !disableBuilder.orElse( false );
158+
}
159+
160+
private static Optional<Boolean> findDisableBuilder(@Nullable PsiModifierListOwner listOwner,
161+
String annotationName) {
162+
PsiAnnotation requestedAnnotation = findAnnotation( listOwner, true, annotationName );
163+
if ( requestedAnnotation != null ) {
164+
PsiNameValuePair builderAttribute = AnnotationUtil.findDeclaredAttribute( requestedAnnotation, "builder" );
165+
if ( builderAttribute != null ) {
166+
PsiAnnotationMemberValue builderValue = builderAttribute.getValue();
167+
if ( builderValue instanceof PsiAnnotation ) {
168+
Boolean disableBuilder = getBooleanAttributeValue( (PsiAnnotation) builderValue, "disableBuilder" );
169+
return Optional.ofNullable( disableBuilder );
170+
}
171+
}
172+
}
173+
174+
return Optional.empty();
175+
}
176+
114177
private static Map<String, Pair<PsiParameter, PsiSubstitutor>> constructorParameters(@NotNull PsiClass psiClass) {
115178
PsiMethod constructor = resolveMappingConstructor( psiClass );
116179
if ( constructor == null || !constructor.hasParameters() ) {
@@ -253,19 +316,19 @@ else if ( methodName.startsWith( "set" ) ) {
253316
* Resolve the builder or self class for the {@code psiType}.
254317
*
255318
* @param psiType the type for which the {@link PsiClass} needs to be resolved
256-
* @param builderSupportPresent whether MapStruct (1.3) with builder support is present
319+
* @param builderEnabled whether MapStruct (1.3) with builder support is present
257320
*
258321
* @return the pair containing the {@link PsiClass} and the corresponding {@link PsiType}
259322
*/
260323
public static Pair<PsiClass, TargetType> resolveBuilderOrSelfClass(@NotNull PsiType psiType,
261-
boolean builderSupportPresent) {
324+
boolean builderEnabled) {
262325
PsiClass psiClass = PsiUtil.resolveClassInType( psiType );
263326
if ( psiClass == null ) {
264327
return null;
265328
}
266329
TargetType targetType = TargetType.defaultType( psiType );
267330

268-
if ( builderSupportPresent ) {
331+
if ( builderEnabled ) {
269332
for ( PsiMethod classMethod : psiClass.getMethods() ) {
270333
if ( MapstructUtil.isPossibleBuilderCreationMethod( classMethod, targetType.type() ) &&
271334
hasBuildMethod( classMethod.getReturnType(), psiType ) ) {
@@ -357,7 +420,8 @@ public static Stream<String> findAllSourcePropertiesForCurrentTarget(@NotNull Ps
357420
*
358421
* @return all target properties for the given {@code targetClass}
359422
*/
360-
public static Set<String> findAllTargetProperties(@NotNull PsiType targetType, MapStructVersion mapStructVersion) {
361-
return publicWriteAccessors( targetType, mapStructVersion ).keySet();
423+
public static Set<String> findAllTargetProperties(@NotNull PsiType targetType, MapStructVersion mapStructVersion,
424+
PsiMethod mappingMethod) {
425+
return publicWriteAccessors( targetType, mapStructVersion, mappingMethod ).keySet();
362426
}
363427
}

src/test/java/org/mapstruct/intellij/MapstructCompletionTestCase.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,4 +952,44 @@ public void testMapperWithGenericBuilder() {
952952
);
953953
}
954954

955+
public void testMapperWithBuilderAndBeanMappingDisabledBuilder() {
956+
configureByTestName();
957+
958+
assertThat( myItems )
959+
.extracting( LookupElement::getLookupString )
960+
.containsExactlyInAnyOrder(
961+
"targetValue"
962+
);
963+
}
964+
965+
public void testMapperWithBuilderAndMapperDisabledBuilder() {
966+
configureByTestName();
967+
968+
assertThat( myItems )
969+
.extracting( LookupElement::getLookupString )
970+
.containsExactlyInAnyOrder(
971+
"targetValue"
972+
);
973+
}
974+
975+
public void testMapperWithBuilderAndMapperDisabledBuilderAndBeanMappingEnable() {
976+
configureByTestName();
977+
978+
assertThat( myItems )
979+
.extracting( LookupElement::getLookupString )
980+
.containsExactlyInAnyOrder(
981+
"builderValue"
982+
);
983+
}
984+
985+
public void testMapperWithBuilderAndMapperDisabledBuilderAndBeanMappingOther() {
986+
configureByTestName();
987+
988+
assertThat( myItems )
989+
.extracting( LookupElement::getLookupString )
990+
.containsExactlyInAnyOrder(
991+
"targetValue"
992+
);
993+
}
994+
955995
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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.inspection;
7+
8+
import java.util.List;
9+
10+
import com.intellij.codeInsight.intention.IntentionAction;
11+
import org.jetbrains.annotations.NotNull;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
15+
/**
16+
* @author Filip Hrisafov
17+
*/
18+
public class UnmappedTargetPropertiesWithBuilderInspectionTest extends BaseInspectionTest {
19+
20+
@NotNull
21+
@Override
22+
protected Class<UnmappedTargetPropertiesInspection> getInspection() {
23+
return UnmappedTargetPropertiesInspection.class;
24+
}
25+
26+
@Override
27+
protected void setUp() throws Exception {
28+
super.setUp();
29+
myFixture.copyFileToProject(
30+
"UnmappedTargetPropertiesWithBuilderData.java",
31+
"org/example/data/UnmappedTargetPropertiesWithBuilderData.java"
32+
);
33+
}
34+
35+
public void testUnmappedTargetPropertiesWithBuilder() {
36+
doTest();
37+
String testName = getTestName( false );
38+
List<IntentionAction> allQuickFixes = myFixture.getAllQuickFixes();
39+
40+
assertThat( allQuickFixes )
41+
.extracting( IntentionAction::getText )
42+
.as( "Intent Text" )
43+
.containsExactly(
44+
"Ignore unmapped target property: 'builderTestName'",
45+
"Add unmapped target property: 'builderTestName'",
46+
47+
"Ignore unmapped target property: 'targetTestName'",
48+
"Add unmapped target property: 'targetTestName'",
49+
50+
"Ignore unmapped target property: 'targetTestName'",
51+
"Add unmapped target property: 'targetTestName'",
52+
53+
"Ignore unmapped target property: 'builderTestName'",
54+
"Add unmapped target property: 'builderTestName'"
55+
);
56+
57+
allQuickFixes.forEach( myFixture::launchAction );
58+
myFixture.checkResultByFile( testName + "_after.java" );
59+
}
60+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.BeanMapping;
8+
import org.mapstruct.Builder;
9+
import org.mapstruct.Mapper;
10+
import org.mapstruct.Mapping;
11+
import org.example.data.UnmappedTargetPropertiesData.Target;
12+
13+
@Mapper
14+
interface DefaultMapper {
15+
16+
Target <warning descr="Unmapped target property: builderTestName">map</warning>(String source);
17+
}
18+
19+
@Mapper(builder = @Builder(disableBuilder = true))
20+
interface MapperDisabledBuilder {
21+
22+
Target <warning descr="Unmapped target property: targetTestName">map</warning>(String source);
23+
}
24+
25+
@Mapper
26+
interface BeanMappingDisabledBuilder {
27+
28+
@BeanMapping(builder = @Builder(disableBuilder = true))
29+
Target <warning descr="Unmapped target property: targetTestName">map</warning>(String source);
30+
}
31+
32+
@Mapper(builder = @Builder(disableBuilder = true))
33+
interface MapperDisabledBuilderBeanMappingEnabledBuilder {
34+
35+
@BeanMapping(builder = @Builder(disableBuilder = false))
36+
Target <warning descr="Unmapped target property: builderTestName">map</warning>(String source);
37+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
public class UnmappedTargetPropertiesData {
9+
public static class Target {
10+
11+
private String targetTestName;
12+
13+
public String getTargetTestName() {
14+
return targetTestName;
15+
}
16+
17+
public void setTargetTestName(String targetTestName) {
18+
this.targetTestName = targetTestName;
19+
}
20+
21+
public static Builder builder() {
22+
return new Builder();
23+
}
24+
25+
public static class Builder {
26+
27+
public Builder builderTestName(String testName) {
28+
29+
}
30+
31+
public Target build() {
32+
return null;
33+
}
34+
}
35+
}
36+
37+
}

0 commit comments

Comments
 (0)