Skip to content

Commit 36453c3

Browse files
authored
#124 Add custom Immutables and FreeBuilder fluent setter MapstructUtils
1 parent cf8d6b6 commit 36453c3

13 files changed

+419
-19
lines changed

build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ dependencies {
114114
testImplementation('org.assertj:assertj-core:3.26.3')
115115
testImplementation('org.apache.commons:commons-text:1.12.0')
116116
testImplementation( 'junit:junit:4.13.2' )
117+
testRuntimeOnly('org.immutables:value:2.10.1')
117118
}
118119

119120
task libs(type: Sync) {
@@ -126,6 +127,13 @@ task libs(type: Sync) {
126127
rename 'mapstruct-1.5.3.Final.jar', 'mapstruct.jar'
127128
}
128129

130+
task testLibs(type: Sync) {
131+
from configurations.testRuntimeClasspath
132+
into "$buildDir/test-libs"
133+
rename 'value-2.10.1.jar', 'immutables.jar'
134+
}
135+
136+
test.dependsOn( libs, testLibs )
129137
prepareSandbox.dependsOn( libs )
130138
composedJar.dependsOn( libs )
131139

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ abstract class MapstructBaseReference extends BaseReference {
3434

3535
private final MapstructBaseReference previous;
3636
private final String value;
37+
protected final MapstructUtil mapstructUtil;
3738

3839
/**
3940
* Create a reference.
@@ -47,6 +48,7 @@ abstract class MapstructBaseReference extends BaseReference {
4748
super( element, rangeInElement );
4849
this.previous = previous;
4950
this.value = value;
51+
this.mapstructUtil = MapstructUtil.getInstance( element.getContainingFile() );
5052
}
5153

5254
@Override

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ builderSupportPresent && isBuilderEnabled( getMappingMethod() )
106106
if ( builderSupportPresent ) {
107107
for ( PsiMethod method : psiClass.findMethodsByName( value, true ) ) {
108108
if ( method.getParameterList().getParametersCount() == 1 &&
109-
MapstructUtil.isFluentSetter( method, typeToUse ) ) {
109+
mapstructUtil.isFluentSetter( method, typeToUse ) ) {
110110
return method;
111111
}
112112
}
@@ -151,6 +151,7 @@ Object[] getVariantsInternal(@NotNull PsiType psiType) {
151151
Map<String, Pair<? extends PsiElement, PsiSubstitutor>> accessors = publicWriteAccessors(
152152
psiType,
153153
mapStructVersion,
154+
mapstructUtil,
154155
mappingMethod
155156
);
156157

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,23 @@ public class FromMapMappingMapTypeInspection extends InspectionBase {
4040
@NotNull
4141
@Override
4242
PsiElementVisitor buildVisitorInternal(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
43-
return new MyJavaElementVisitor( holder, MapstructUtil.resolveMapStructProjectVersion( holder.getFile() ) );
43+
return new MyJavaElementVisitor(
44+
holder,
45+
MapstructUtil.resolveMapStructProjectVersion( holder.getFile() ),
46+
MapstructUtil.getInstance( holder.getFile() )
47+
);
4448
}
4549

4650
private static class MyJavaElementVisitor extends JavaElementVisitor {
4751
private final ProblemsHolder holder;
4852
private final MapStructVersion mapStructVersion;
53+
private final MapstructUtil mapstructUtil;
4954

50-
private MyJavaElementVisitor(ProblemsHolder holder, MapStructVersion mapStructVersion) {
55+
private MyJavaElementVisitor(ProblemsHolder holder, MapStructVersion mapStructVersion,
56+
MapstructUtil mapstructUtil) {
5157
this.holder = holder;
5258
this.mapStructVersion = mapStructVersion;
59+
this.mapstructUtil = mapstructUtil;
5360
}
5461

5562
@Override
@@ -73,7 +80,12 @@ public void visitMethod(@NotNull PsiMethod method) {
7380
if (parameters == null) {
7481
return;
7582
}
76-
Set<String> allTargetProperties = findAllTargetProperties( targetType, mapStructVersion, method );
83+
Set<String> allTargetProperties = findAllTargetProperties(
84+
targetType,
85+
mapStructVersion,
86+
mapstructUtil,
87+
method
88+
);
7789
if ( allTargetProperties.contains( fromMapMappingParameter.getName() ) ) {
7890
return;
7991
}

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,23 @@ public class UnmappedTargetPropertiesInspection extends InspectionBase {
5959
@NotNull
6060
@Override
6161
PsiElementVisitor buildVisitorInternal(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
62-
return new MyJavaElementVisitor( holder, MapstructUtil.resolveMapStructProjectVersion( holder.getFile() ) );
62+
return new MyJavaElementVisitor(
63+
holder,
64+
MapstructUtil.resolveMapStructProjectVersion( holder.getFile() ),
65+
MapstructUtil.getInstance( holder.getFile() )
66+
);
6367
}
6468

6569
private static class MyJavaElementVisitor extends JavaElementVisitor {
6670
private final ProblemsHolder holder;
6771
private final MapStructVersion mapStructVersion;
72+
private final MapstructUtil mapstructUtil;
6873

69-
private MyJavaElementVisitor(ProblemsHolder holder, MapStructVersion mapStructVersion) {
74+
private MyJavaElementVisitor(ProblemsHolder holder, MapStructVersion mapStructVersion,
75+
MapstructUtil mapstructUtil) {
7076
this.holder = holder;
7177
this.mapStructVersion = mapStructVersion;
78+
this.mapstructUtil = mapstructUtil;
7279
}
7380

7481
@Override
@@ -97,7 +104,12 @@ public void visitMethod(PsiMethod method) {
97104
return;
98105
}
99106

100-
Set<String> allTargetProperties = findAllTargetProperties( targetType, mapStructVersion, method );
107+
Set<String> allTargetProperties = findAllTargetProperties(
108+
targetType,
109+
mapStructVersion,
110+
mapstructUtil,
111+
method
112+
);
101113

102114
// find and remove all defined mapping targets
103115
Set<String> definedTargets = findAllDefinedMappingTargets( method, mapStructVersion )
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright MapStruct Authors.
3+
*
4+
* Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
package org.mapstruct.intellij.util;
7+
8+
import com.intellij.psi.PsiMethod;
9+
import com.intellij.psi.PsiType;
10+
import org.jetbrains.annotations.NotNull;
11+
12+
/**
13+
* Mapstruct util for FreeBuilder.
14+
* FreeBuilder adds a lot of other methods that can be considered as fluent setters. Such as:
15+
* <ul>
16+
* <li>{@code from(Target)}</li>
17+
* <li>{@code mapXXX(UnaryOperator)}</li>
18+
* <li>{@code mutateXXX(Consumer)}</li>
19+
* <li>{@code mergeFrom(Target)}</li>
20+
* <li>{@code mergeFrom(Target.Builder)}</li>
21+
* </ul>
22+
* <p>
23+
* When the JavaBean convention is not used with FreeBuilder then the getters are non-standard and MapStruct
24+
* won't recognize them. Therefore, one needs to use the JavaBean convention in which the fluent setters
25+
* start with {@code set}.
26+
*/
27+
public class FreeBuildersMapstructUtil extends MapstructUtil {
28+
29+
static final MapstructUtil INSTANCE = new FreeBuildersMapstructUtil();
30+
31+
/**
32+
* Hide constructor.
33+
*/
34+
private FreeBuildersMapstructUtil() {
35+
}
36+
37+
@Override
38+
public boolean isFluentSetter(@NotNull PsiMethod method, PsiType psiType) {
39+
// When using FreeBuilder one needs to use the JavaBean convention,
40+
// which means that all setters will start with set
41+
return false;
42+
}
43+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright MapStruct Authors.
3+
*
4+
* Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
package org.mapstruct.intellij.util;
7+
8+
import com.intellij.psi.PsiMethod;
9+
import com.intellij.psi.PsiType;
10+
import org.jetbrains.annotations.NotNull;
11+
12+
/**
13+
* Mapstruct util for Immutables.
14+
* The generated Immutables also have a from that works as a copy. Our default strategy considers this method
15+
* as a setter with a name {@code from}. Therefore, we are ignoring it.
16+
*/
17+
public class ImmutablesMapstructUtil extends MapstructUtil {
18+
19+
static final MapstructUtil INSTANCE = new ImmutablesMapstructUtil();
20+
21+
/**
22+
* Hide constructor.
23+
*/
24+
private ImmutablesMapstructUtil() {
25+
}
26+
27+
@Override
28+
public boolean isFluentSetter(@NotNull PsiMethod method, PsiType psiType) {
29+
return super.isFluentSetter( method, psiType ) && !method.getName().equals( "from" );
30+
}
31+
}

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

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.intellij.openapi.module.ModuleUtilCore;
2222
import com.intellij.openapi.roots.ProjectRootManager;
2323
import com.intellij.openapi.util.Pair;
24+
import com.intellij.openapi.vfs.VirtualFile;
2425
import com.intellij.psi.CommonClassNames;
2526
import com.intellij.psi.EmptySubstitutor;
2627
import com.intellij.psi.JavaPsiFacade;
@@ -70,7 +71,9 @@
7071
/**
7172
* @author Filip Hrisafov
7273
*/
73-
public final class MapstructUtil {
74+
public class MapstructUtil {
75+
76+
private static final MapstructUtil INSTANCE = new MapstructUtil();
7477

7578
/**
7679
* The FQN of the {@link Mapper} annotation.
@@ -101,11 +104,29 @@ public final class MapstructUtil {
101104
private static final String CONTEXT_ANNOTATION_FQN = Context.class.getName();
102105
private static final String BUILDER_ANNOTATION_FQN = Builder.class.getName();
103106
private static final String ENUM_MAPPING_ANNOTATION_FQN = EnumMapping.class.getName();
107+
private static final String IMMUTABLE_FQN = "org.immutables.value.Value.Immutable";
108+
private static final String FREE_BUILDER_FQN = "org.inferred.freebuilder.FreeBuilder";
104109

105110
/**
106111
* Hide constructor.
107112
*/
108-
private MapstructUtil() {
113+
MapstructUtil() {
114+
}
115+
116+
public static MapstructUtil getInstance(@Nullable PsiFile psiFile) {
117+
if ( psiFile == null ) {
118+
return MapstructUtil.INSTANCE;
119+
}
120+
121+
if ( MapstructUtil.immutablesOnClassPath( psiFile ) ) {
122+
return ImmutablesMapstructUtil.INSTANCE;
123+
}
124+
125+
if ( MapstructUtil.freeBuilderOnClassPath( psiFile ) ) {
126+
return FreeBuildersMapstructUtil.INSTANCE;
127+
}
128+
129+
return MapstructUtil.INSTANCE;
109130
}
110131

111132
public static LookupElement[] asLookup(Map<String, Pair<? extends PsiElement, PsiSubstitutor>> accessors,
@@ -204,7 +225,7 @@ public static boolean isPublicModifiable(@NotNull PsiField field) {
204225
!field.hasModifierProperty( PsiModifier.FINAL );
205226
}
206227

207-
public static boolean isFluentSetter(@NotNull PsiMethod method, PsiType psiType) {
228+
public boolean isFluentSetter(@NotNull PsiMethod method, PsiType psiType) {
208229
return !psiType.getCanonicalText().startsWith( "java.lang" ) &&
209230
method.getReturnType() != null &&
210231
!isAdderWithUpperCase4thCharacter( method ) &&
@@ -560,6 +581,44 @@ else if ( JavaPsiFacade.getInstance( module.getProject() )
560581
} );
561582
}
562583

584+
private static boolean immutablesOnClassPath(@NotNull PsiFile psiFile) {
585+
VirtualFile virtualFile = psiFile.getVirtualFile();
586+
if ( virtualFile == null ) {
587+
return false;
588+
}
589+
Module module = ModuleUtilCore.findModuleForFile( virtualFile, psiFile.getProject() );
590+
if ( module == null ) {
591+
return false;
592+
}
593+
return CachedValuesManager.getManager( module.getProject() ).getCachedValue( module, () -> {
594+
boolean immutablesOnClassPath = JavaPsiFacade.getInstance( module.getProject() )
595+
.findClass( IMMUTABLE_FQN, module.getModuleRuntimeScope( false ) ) != null;
596+
return CachedValueProvider.Result.createSingleDependency(
597+
immutablesOnClassPath,
598+
ProjectRootManager.getInstance( module.getProject() )
599+
);
600+
} );
601+
}
602+
603+
private static boolean freeBuilderOnClassPath(@NotNull PsiFile psiFile) {
604+
VirtualFile virtualFile = psiFile.getVirtualFile();
605+
if ( virtualFile == null ) {
606+
return false;
607+
}
608+
Module module = ModuleUtilCore.findModuleForFile( virtualFile, psiFile.getProject() );
609+
if ( module == null ) {
610+
return false;
611+
}
612+
return CachedValuesManager.getManager( module.getProject() ).getCachedValue( module, () -> {
613+
boolean freeBuilderOnClassPath = JavaPsiFacade.getInstance( module.getProject() )
614+
.findClass( FREE_BUILDER_FQN, module.getModuleRuntimeScope( false ) ) != null;
615+
return CachedValueProvider.Result.createSingleDependency(
616+
freeBuilderOnClassPath,
617+
ProjectRootManager.getInstance( module.getProject() )
618+
);
619+
} );
620+
}
621+
563622
/**
564623
* Checks if MapStruct jdk8 is within the provided module. The MapStruct JDK 8 module is present when the
565624
* {@link Mapping} annotation is annotated with {@link java.lang.annotation.Repeatable}

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
import static org.mapstruct.intellij.util.MapstructAnnotationUtils.findMapperConfigReference;
4747
import static org.mapstruct.intellij.util.MapstructUtil.MAPPER_ANNOTATION_FQN;
4848
import static org.mapstruct.intellij.util.MapstructUtil.canDescendIntoType;
49-
import static org.mapstruct.intellij.util.MapstructUtil.isFluentSetter;
5049
import static org.mapstruct.intellij.util.MapstructUtil.isInheritInverseConfiguration;
5150
import static org.mapstruct.intellij.util.MapstructUtil.isMapper;
5251
import static org.mapstruct.intellij.util.MapstructUtil.isMapperConfig;
@@ -100,7 +99,7 @@ public static PsiType getRelevantType(@NotNull PsiMethod mappingMethod) {
10099
* @return a stream that holds all public write accessors for the given {@code psiType}
101100
*/
102101
public static Map<String, Pair<? extends PsiElement, PsiSubstitutor>> publicWriteAccessors(@NotNull PsiType psiType,
103-
MapStructVersion mapStructVersion, PsiMethod mappingMethod) {
102+
MapStructVersion mapStructVersion, MapstructUtil mapstructUtil, PsiMethod mappingMethod) {
104103
boolean builderSupportPresent = mapStructVersion.isBuilderSupported();
105104
Pair<PsiClass, TargetType> classAndType = resolveBuilderOrSelfClass(
106105
psiType,
@@ -116,7 +115,8 @@ builderSupportPresent && isBuilderEnabled( mappingMethod )
116115
TargetType targetType = classAndType.getSecond();
117116
PsiType typeToUse = targetType.type();
118117

119-
publicWriteAccessors.putAll( publicSetters( psiClass, typeToUse, builderSupportPresent ) );
118+
publicWriteAccessors.putAll( publicSetters( psiClass, typeToUse, mapstructUtil,
119+
builderSupportPresent && isBuilderEnabled( mappingMethod ) ) );
120120
publicWriteAccessors.putAll( publicFields( psiClass ) );
121121

122122
if ( mapStructVersion.isConstructorSupported() && !targetType.builder() ) {
@@ -268,7 +268,7 @@ public static PsiMethod resolveMappingConstructor(@NotNull PsiClass psiClass) {
268268
* @return a stream that holds all public setters for the given {@code psiType}
269269
*/
270270
private static Map<String, Pair<? extends PsiMember, PsiSubstitutor>> publicSetters(@NotNull PsiClass psiClass,
271-
@NotNull PsiType typeToUse,
271+
@NotNull PsiType typeToUse, MapstructUtil mapstructUtil,
272272
boolean builderSupportPresent) {
273273
Set<PsiMethod> overriddenMethods = new HashSet<>();
274274
Map<String, Pair<? extends PsiMember, PsiSubstitutor>> publicSetters = new LinkedHashMap<>();
@@ -277,7 +277,12 @@ private static Map<String, Pair<? extends PsiMember, PsiSubstitutor>> publicSett
277277
if ( method.isConstructor() ) {
278278
continue;
279279
}
280-
String propertyName = extractPublicSetterPropertyName( method, typeToUse, builderSupportPresent );
280+
String propertyName = extractPublicSetterPropertyName(
281+
method,
282+
typeToUse,
283+
mapstructUtil,
284+
builderSupportPresent
285+
);
281286

282287
if ( propertyName != null &&
283288
!overriddenMethods.contains( method ) ) {
@@ -292,7 +297,7 @@ private static Map<String, Pair<? extends PsiMember, PsiSubstitutor>> publicSett
292297

293298
@Nullable
294299
private static String extractPublicSetterPropertyName(PsiMethod method, @NotNull PsiType typeToUse,
295-
boolean builderSupportPresent) {
300+
MapstructUtil mapstructUtil, boolean builderSupportPresent) {
296301
if (!MapstructUtil.isPublicNonStatic( method )) {
297302
// If the method is not public then there is no property
298303
return null;
@@ -312,7 +317,7 @@ private static String extractPublicSetterPropertyName(PsiMethod method, @NotNull
312317
}
313318

314319
// This logic is aligned with the DefaultAccessorNamingStrategy
315-
if ( builderSupportPresent && isFluentSetter( method, typeToUse )) {
320+
if ( builderSupportPresent && mapstructUtil.isFluentSetter( method, typeToUse )) {
316321
if ( methodName.startsWith( "set" )
317322
&& methodName.length() > 3
318323
&& Character.isUpperCase( methodName.charAt( 3 ) ) ) {
@@ -435,8 +440,8 @@ public static Stream<String> findAllSourcePropertiesForCurrentTarget(@NotNull Ps
435440
* @return all target properties for the given {@code targetClass}
436441
*/
437442
public static Set<String> findAllTargetProperties(@NotNull PsiType targetType, MapStructVersion mapStructVersion,
438-
PsiMethod mappingMethod) {
439-
return publicWriteAccessors( targetType, mapStructVersion, mappingMethod ).keySet();
443+
MapstructUtil mapstructUtil, PsiMethod mappingMethod) {
444+
return publicWriteAccessors( targetType, mapStructVersion, mapstructUtil, mappingMethod ).keySet();
440445
}
441446

442447
/**

0 commit comments

Comments
 (0)