Skip to content

#124 Add custom Immutables and FreeBuilder fluent setter MapstructUtils #125

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 10, 2025
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
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ dependencies {
testImplementation('org.assertj:assertj-core:3.26.3')
testImplementation('org.apache.commons:commons-text:1.12.0')
testImplementation( 'junit:junit:4.13.2' )
testRuntimeOnly('org.immutables:value:2.10.1')
}

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

task testLibs(type: Sync) {
from configurations.testRuntimeClasspath
into "$buildDir/test-libs"
rename 'value-2.10.1.jar', 'immutables.jar'
}

test.dependsOn( libs, testLibs )
prepareSandbox.dependsOn( libs )
composedJar.dependsOn( libs )

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ abstract class MapstructBaseReference extends BaseReference {

private final MapstructBaseReference previous;
private final String value;
protected final MapstructUtil mapstructUtil;

/**
* Create a reference.
Expand All @@ -47,6 +48,7 @@ abstract class MapstructBaseReference extends BaseReference {
super( element, rangeInElement );
this.previous = previous;
this.value = value;
this.mapstructUtil = MapstructUtil.getInstance( element.getContainingFile() );
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ builderSupportPresent && isBuilderEnabled( getMappingMethod() )
if ( builderSupportPresent ) {
for ( PsiMethod method : psiClass.findMethodsByName( value, true ) ) {
if ( method.getParameterList().getParametersCount() == 1 &&
MapstructUtil.isFluentSetter( method, typeToUse ) ) {
mapstructUtil.isFluentSetter( method, typeToUse ) ) {
return method;
}
}
Expand Down Expand Up @@ -151,6 +151,7 @@ Object[] getVariantsInternal(@NotNull PsiType psiType) {
Map<String, Pair<? extends PsiElement, PsiSubstitutor>> accessors = publicWriteAccessors(
psiType,
mapStructVersion,
mapstructUtil,
mappingMethod
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,23 @@ public class FromMapMappingMapTypeInspection extends InspectionBase {
@NotNull
@Override
PsiElementVisitor buildVisitorInternal(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
return new MyJavaElementVisitor( holder, MapstructUtil.resolveMapStructProjectVersion( holder.getFile() ) );
return new MyJavaElementVisitor(
holder,
MapstructUtil.resolveMapStructProjectVersion( holder.getFile() ),
MapstructUtil.getInstance( holder.getFile() )
);
}

private static class MyJavaElementVisitor extends JavaElementVisitor {
private final ProblemsHolder holder;
private final MapStructVersion mapStructVersion;
private final MapstructUtil mapstructUtil;

private MyJavaElementVisitor(ProblemsHolder holder, MapStructVersion mapStructVersion) {
private MyJavaElementVisitor(ProblemsHolder holder, MapStructVersion mapStructVersion,
MapstructUtil mapstructUtil) {
this.holder = holder;
this.mapStructVersion = mapStructVersion;
this.mapstructUtil = mapstructUtil;
}

@Override
Expand All @@ -73,7 +80,12 @@ public void visitMethod(@NotNull PsiMethod method) {
if (parameters == null) {
return;
}
Set<String> allTargetProperties = findAllTargetProperties( targetType, mapStructVersion, method );
Set<String> allTargetProperties = findAllTargetProperties(
targetType,
mapStructVersion,
mapstructUtil,
method
);
if ( allTargetProperties.contains( fromMapMappingParameter.getName() ) ) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,23 @@ public class UnmappedTargetPropertiesInspection extends InspectionBase {
@NotNull
@Override
PsiElementVisitor buildVisitorInternal(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
return new MyJavaElementVisitor( holder, MapstructUtil.resolveMapStructProjectVersion( holder.getFile() ) );
return new MyJavaElementVisitor(
holder,
MapstructUtil.resolveMapStructProjectVersion( holder.getFile() ),
MapstructUtil.getInstance( holder.getFile() )
);
}

private static class MyJavaElementVisitor extends JavaElementVisitor {
private final ProblemsHolder holder;
private final MapStructVersion mapStructVersion;
private final MapstructUtil mapstructUtil;

private MyJavaElementVisitor(ProblemsHolder holder, MapStructVersion mapStructVersion) {
private MyJavaElementVisitor(ProblemsHolder holder, MapStructVersion mapStructVersion,
MapstructUtil mapstructUtil) {
this.holder = holder;
this.mapStructVersion = mapStructVersion;
this.mapstructUtil = mapstructUtil;
}

@Override
Expand Down Expand Up @@ -97,7 +104,12 @@ public void visitMethod(PsiMethod method) {
return;
}

Set<String> allTargetProperties = findAllTargetProperties( targetType, mapStructVersion, method );
Set<String> allTargetProperties = findAllTargetProperties(
targetType,
mapStructVersion,
mapstructUtil,
method
);

// find and remove all defined mapping targets
Set<String> definedTargets = findAllDefinedMappingTargets( method, mapStructVersion )
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.intellij.util;

import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiType;
import org.jetbrains.annotations.NotNull;

/**
* Mapstruct util for FreeBuilder.
* FreeBuilder adds a lot of other methods that can be considered as fluent setters. Such as:
* <ul>
* <li>{@code from(Target)}</li>
* <li>{@code mapXXX(UnaryOperator)}</li>
* <li>{@code mutateXXX(Consumer)}</li>
* <li>{@code mergeFrom(Target)}</li>
* <li>{@code mergeFrom(Target.Builder)}</li>
* </ul>
* <p>
* When the JavaBean convention is not used with FreeBuilder then the getters are non-standard and MapStruct
* won't recognize them. Therefore, one needs to use the JavaBean convention in which the fluent setters
* start with {@code set}.
*/
public class FreeBuildersMapstructUtil extends MapstructUtil {

static final MapstructUtil INSTANCE = new FreeBuildersMapstructUtil();

/**
* Hide constructor.
*/
private FreeBuildersMapstructUtil() {
}

@Override
public boolean isFluentSetter(@NotNull PsiMethod method, PsiType psiType) {
// When using FreeBuilder one needs to use the JavaBean convention,
// which means that all setters will start with set
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at https://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.intellij.util;

import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiType;
import org.jetbrains.annotations.NotNull;

/**
* Mapstruct util for Immutables.
* The generated Immutables also have a from that works as a copy. Our default strategy considers this method
* as a setter with a name {@code from}. Therefore, we are ignoring it.
*/
public class ImmutablesMapstructUtil extends MapstructUtil {

static final MapstructUtil INSTANCE = new ImmutablesMapstructUtil();

/**
* Hide constructor.
*/
private ImmutablesMapstructUtil() {
}

@Override
public boolean isFluentSetter(@NotNull PsiMethod method, PsiType psiType) {
return super.isFluentSetter( method, psiType ) && !method.getName().equals( "from" );
}
}
65 changes: 62 additions & 3 deletions src/main/java/org/mapstruct/intellij/util/MapstructUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.CommonClassNames;
import com.intellij.psi.EmptySubstitutor;
import com.intellij.psi.JavaPsiFacade;
Expand Down Expand Up @@ -70,7 +71,9 @@
/**
* @author Filip Hrisafov
*/
public final class MapstructUtil {
public class MapstructUtil {

private static final MapstructUtil INSTANCE = new MapstructUtil();

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

/**
* Hide constructor.
*/
private MapstructUtil() {
MapstructUtil() {
}

public static MapstructUtil getInstance(@Nullable PsiFile psiFile) {
if ( psiFile == null ) {
return MapstructUtil.INSTANCE;
}

if ( MapstructUtil.immutablesOnClassPath( psiFile ) ) {
return ImmutablesMapstructUtil.INSTANCE;
}

if ( MapstructUtil.freeBuilderOnClassPath( psiFile ) ) {
return FreeBuildersMapstructUtil.INSTANCE;
}

return MapstructUtil.INSTANCE;
}

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

public static boolean isFluentSetter(@NotNull PsiMethod method, PsiType psiType) {
public boolean isFluentSetter(@NotNull PsiMethod method, PsiType psiType) {
return !psiType.getCanonicalText().startsWith( "java.lang" ) &&
method.getReturnType() != null &&
!isAdderWithUpperCase4thCharacter( method ) &&
Expand Down Expand Up @@ -560,6 +581,44 @@ else if ( JavaPsiFacade.getInstance( module.getProject() )
} );
}

private static boolean immutablesOnClassPath(@NotNull PsiFile psiFile) {
VirtualFile virtualFile = psiFile.getVirtualFile();
if ( virtualFile == null ) {
return false;
}
Module module = ModuleUtilCore.findModuleForFile( virtualFile, psiFile.getProject() );
if ( module == null ) {
return false;
}
return CachedValuesManager.getManager( module.getProject() ).getCachedValue( module, () -> {
boolean immutablesOnClassPath = JavaPsiFacade.getInstance( module.getProject() )
.findClass( IMMUTABLE_FQN, module.getModuleRuntimeScope( false ) ) != null;
return CachedValueProvider.Result.createSingleDependency(
immutablesOnClassPath,
ProjectRootManager.getInstance( module.getProject() )
);
} );
}

private static boolean freeBuilderOnClassPath(@NotNull PsiFile psiFile) {
VirtualFile virtualFile = psiFile.getVirtualFile();
if ( virtualFile == null ) {
return false;
}
Module module = ModuleUtilCore.findModuleForFile( virtualFile, psiFile.getProject() );
if ( module == null ) {
return false;
}
return CachedValuesManager.getManager( module.getProject() ).getCachedValue( module, () -> {
boolean freeBuilderOnClassPath = JavaPsiFacade.getInstance( module.getProject() )
.findClass( FREE_BUILDER_FQN, module.getModuleRuntimeScope( false ) ) != null;
return CachedValueProvider.Result.createSingleDependency(
freeBuilderOnClassPath,
ProjectRootManager.getInstance( module.getProject() )
);
} );
}

/**
* Checks if MapStruct jdk8 is within the provided module. The MapStruct JDK 8 module is present when the
* {@link Mapping} annotation is annotated with {@link java.lang.annotation.Repeatable}
Expand Down
23 changes: 14 additions & 9 deletions src/main/java/org/mapstruct/intellij/util/TargetUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
import static org.mapstruct.intellij.util.MapstructAnnotationUtils.findMapperConfigReference;
import static org.mapstruct.intellij.util.MapstructUtil.MAPPER_ANNOTATION_FQN;
import static org.mapstruct.intellij.util.MapstructUtil.canDescendIntoType;
import static org.mapstruct.intellij.util.MapstructUtil.isFluentSetter;
import static org.mapstruct.intellij.util.MapstructUtil.isInheritInverseConfiguration;
import static org.mapstruct.intellij.util.MapstructUtil.isMapper;
import static org.mapstruct.intellij.util.MapstructUtil.isMapperConfig;
Expand Down Expand Up @@ -100,7 +99,7 @@ public static PsiType getRelevantType(@NotNull PsiMethod mappingMethod) {
* @return a stream that holds all public write accessors for the given {@code psiType}
*/
public static Map<String, Pair<? extends PsiElement, PsiSubstitutor>> publicWriteAccessors(@NotNull PsiType psiType,
MapStructVersion mapStructVersion, PsiMethod mappingMethod) {
MapStructVersion mapStructVersion, MapstructUtil mapstructUtil, PsiMethod mappingMethod) {
boolean builderSupportPresent = mapStructVersion.isBuilderSupported();
Pair<PsiClass, TargetType> classAndType = resolveBuilderOrSelfClass(
psiType,
Expand All @@ -116,7 +115,8 @@ builderSupportPresent && isBuilderEnabled( mappingMethod )
TargetType targetType = classAndType.getSecond();
PsiType typeToUse = targetType.type();

publicWriteAccessors.putAll( publicSetters( psiClass, typeToUse, builderSupportPresent ) );
publicWriteAccessors.putAll( publicSetters( psiClass, typeToUse, mapstructUtil,
builderSupportPresent && isBuilderEnabled( mappingMethod ) ) );
publicWriteAccessors.putAll( publicFields( psiClass ) );

if ( mapStructVersion.isConstructorSupported() && !targetType.builder() ) {
Expand Down Expand Up @@ -268,7 +268,7 @@ public static PsiMethod resolveMappingConstructor(@NotNull PsiClass psiClass) {
* @return a stream that holds all public setters for the given {@code psiType}
*/
private static Map<String, Pair<? extends PsiMember, PsiSubstitutor>> publicSetters(@NotNull PsiClass psiClass,
@NotNull PsiType typeToUse,
@NotNull PsiType typeToUse, MapstructUtil mapstructUtil,
boolean builderSupportPresent) {
Set<PsiMethod> overriddenMethods = new HashSet<>();
Map<String, Pair<? extends PsiMember, PsiSubstitutor>> publicSetters = new LinkedHashMap<>();
Expand All @@ -277,7 +277,12 @@ private static Map<String, Pair<? extends PsiMember, PsiSubstitutor>> publicSett
if ( method.isConstructor() ) {
continue;
}
String propertyName = extractPublicSetterPropertyName( method, typeToUse, builderSupportPresent );
String propertyName = extractPublicSetterPropertyName(
method,
typeToUse,
mapstructUtil,
builderSupportPresent
);

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

@Nullable
private static String extractPublicSetterPropertyName(PsiMethod method, @NotNull PsiType typeToUse,
boolean builderSupportPresent) {
MapstructUtil mapstructUtil, boolean builderSupportPresent) {
if (!MapstructUtil.isPublicNonStatic( method )) {
// If the method is not public then there is no property
return null;
Expand All @@ -312,7 +317,7 @@ private static String extractPublicSetterPropertyName(PsiMethod method, @NotNull
}

// This logic is aligned with the DefaultAccessorNamingStrategy
if ( builderSupportPresent && isFluentSetter( method, typeToUse )) {
if ( builderSupportPresent && mapstructUtil.isFluentSetter( method, typeToUse )) {
if ( methodName.startsWith( "set" )
&& methodName.length() > 3
&& Character.isUpperCase( methodName.charAt( 3 ) ) ) {
Expand Down Expand Up @@ -435,8 +440,8 @@ public static Stream<String> findAllSourcePropertiesForCurrentTarget(@NotNull Ps
* @return all target properties for the given {@code targetClass}
*/
public static Set<String> findAllTargetProperties(@NotNull PsiType targetType, MapStructVersion mapStructVersion,
PsiMethod mappingMethod) {
return publicWriteAccessors( targetType, mapStructVersion, mappingMethod ).keySet();
MapstructUtil mapstructUtil, PsiMethod mappingMethod) {
return publicWriteAccessors( targetType, mapStructVersion, mapstructUtil, mappingMethod ).keySet();
}

/**
Expand Down
Loading
Loading