Skip to content

Commit

Permalink
#124 Add custom Immutables and FreeBuilder fluent setter MapstructUtils
Browse files Browse the repository at this point in the history
  • Loading branch information
Stephan Leicht Vogt (C803964) committed May 9, 2023
1 parent c27f853 commit 8c3d9ee
Show file tree
Hide file tree
Showing 14 changed files with 386 additions and 22 deletions.
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" );
}
}
53 changes: 49 additions & 4 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 @@ -140,7 +155,7 @@ public static LookupElement asLookup(PsiEnumConstant enumConstant) {
}

public static LookupElement asLookupWithRepresentableText(PsiMethod method, String lookupString,
String representableText, String tailText) {
String representableText, String tailText) {
LookupElementBuilder builder = LookupElementBuilder.create( method, lookupString )
.withIcon( PlatformIcons.METHOD_ICON )
.withPresentableText( representableText )
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 @@ -289,7 +288,7 @@ private static Map<String, Pair<? extends PsiMember, PsiSubstitutor>> publicSett
}

@Nullable
private static String extractPublicSetterPropertyName(PsiMethod method, @NotNull PsiType typeToUse,
private static String extractPublicSetterPropertyName(PsiMethod method, @NotNull PsiType typeToUse, 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
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 @@ -432,9 +431,9 @@ 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,
public static Set<String> findAllTargetProperties(@NotNull PsiType targetType, MapStructVersion mapStructVersion, MapstructUtil mapstructUtil,
PsiMethod mappingMethod) {
return publicWriteAccessors( targetType, mapStructVersion, mappingMethod ).keySet();
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

0 comments on commit 8c3d9ee

Please sign in to comment.