Skip to content
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

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
11 changes: 9 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ dependencies {
testRuntimeOnly('org.junit.vintage:junit-vintage-engine')
testImplementation('org.assertj:assertj-core:3.11.1')
testImplementation('org.apache.commons:commons-text:1.10.0')
testRuntimeOnly('org.immutables:value:2.5.6')
}

task libs(type: Sync) {
Expand All @@ -111,6 +112,12 @@ 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.5.6.jar', 'immutables.jar'
}

def mockJdkLocation = "https://github.com/JetBrains/intellij-community/raw/master/java/mock"
def mockJdkDest = "$buildDir/mock"
def downloadMockJdk(mockJdkLocation, mockJdkDest, mockJdkVersion) {
Expand Down Expand Up @@ -150,8 +157,8 @@ task downloadMockJdk11() {
downloadMockJdk(mockJdkLocation, mockJdkDest, "JDK-11")
}

test.dependsOn( libs, downloadMockJdk7, downloadMockJdk8, downloadMockJdk11 )
prepareTestingSandbox.dependsOn( libs )
test.dependsOn( libs, testLibs, downloadMockJdk7, downloadMockJdk8, downloadMockJdk11 )
prepareTestingSandbox.dependsOn( libs, testLibs )
prepareSandbox.dependsOn( libs )

test {
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 @@ -100,7 +100,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 @@ -140,7 +140,7 @@ PsiElement resolveInternal(@NotNull String value, @NotNull PsiMethod mappingMeth
@Override
Object[] getVariantsInternal(@NotNull PsiType psiType) {
return asLookup(
publicWriteAccessors( psiType, mapStructVersion, getMappingMethod() ),
publicWriteAccessors( psiType, mapStructVersion, mapstructUtil, getMappingMethod() ),
MapstructTargetReference::memberPsiType
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,19 @@ 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
public void visitMethod(PsiMethod method) {
Expand All @@ -92,7 +94,7 @@ public void visitMethod(PsiMethod method) {
}


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,14 @@
/*
* 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;

public class DefaultMapstructUtil extends MapstructUtil {
/**
* Hide constructor.
*/
protected DefaultMapstructUtil() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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 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 {
/**
* Hide constructor.
*/
protected 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,28 @@
/*
* 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 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 {
/**
* Hide constructor.
*/
protected ImmutablesMapstructUtil() {
}

@Override
public boolean isFluentSetter(@NotNull PsiMethod method, PsiType psiType) {
return super.isFluentSetter( method, psiType ) && !method.getName().equals( "from" );
}
}
51 changes: 48 additions & 3 deletions src/main/java/org/mapstruct/intellij/util/MapstructUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
/**
* @author Filip Hrisafov
*/
public final class MapstructUtil {
public class MapstructUtil {

/**
* The FQN of the {@link Mapper} annotation.
Expand Down Expand Up @@ -100,11 +100,26 @@ public final class MapstructUtil {
private static final String INHERIT_INVERSE_CONFIGURATION_FQN = InheritInverseConfiguration.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) {
MapstructUtil mapstructUtil = new DefaultMapstructUtil();
if (psiFile == null) {
return mapstructUtil;
}
if (MapstructUtil.immutablesOnClassPath(psiFile)) {
mapstructUtil = new ImmutablesMapstructUtil();
} else if (MapstructUtil.freeBuilderOnClassPath(psiFile)) {
mapstructUtil = new FreeBuildersMapstructUtil();
}
return mapstructUtil;
}

public static LookupElement[] asLookup(Map<String, Pair<? extends PsiElement, PsiSubstitutor>> accessors,
Expand Down Expand Up @@ -203,7 +218,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 @@ -541,6 +556,36 @@ else if ( JavaPsiFacade.getInstance( module.getProject() )
} );
}

public static boolean immutablesOnClassPath(@NotNull PsiFile psiFile) {
Module module = ModuleUtilCore.findModuleForFile(psiFile.getVirtualFile(), 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())
);
});
}

public static boolean freeBuilderOnClassPath(@NotNull PsiFile psiFile) {
Module module = ModuleUtilCore.findModuleForFile(psiFile.getVirtualFile(), 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
17 changes: 8 additions & 9 deletions src/main/java/org/mapstruct/intellij/util/TargetUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
import static org.mapstruct.intellij.util.MapstructUtil.INHERIT_CONFIGURATION_FQN;
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.publicFields;

/**
Expand Down Expand Up @@ -98,7 +97,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 @@ -114,7 +113,7 @@ 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 @@ -266,7 +265,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 @@ -275,7 +274,7 @@ 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 @@ -290,7 +289,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 ( method.getParameterList().getParametersCount() != 1 || !MapstructUtil.isPublicNonStatic( method ) ) {
// If the method does not have 1 parameter or is not public then there is no property
return null;
Expand All @@ -299,7 +298,7 @@ private static String extractPublicSetterPropertyName(PsiMethod method, @NotNull
// This logic is aligned with the DefaultAccessorNamingStrategy
String methodName = method.getName();
if ( builderSupportPresent ) {
if ( isFluentSetter( method, typeToUse ) ) {
if ( mapstructUtil.isFluentSetter( method, typeToUse ) ) {
if ( methodName.startsWith( "set" )
&& methodName.length() > 3
&& Character.isUpperCase( methodName.charAt( 3 ) ) ) {
Expand Down Expand Up @@ -433,8 +432,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
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
*/
public abstract class MapstructBaseCompletionTestCase extends LightFixtureCompletionTestCase {

private static final String BUILD_LIBS_DIRECTORY = "build/libs";
protected static final String BUILD_LIBS_DIRECTORY = "build/libs";
protected static final String BUILD_TEST_LIBS_DIRECTORY = "build/test-libs";
private static final String BUILD_MOCK_JDK_DIRECTORY = "build/mockJDK-";

@Override
Expand Down
Loading