Add support for Lombok
Add support for Lombok; especially @AllArgsConstructor and @RequiredArgsConstructor which makes we can omit the constructor parameter, so the reference to not work.
Also fixes: if some constructor is protected (e.g., @NoArgsConstructor(access = AccessLevel.PROTECTED) from Lombok), it also should be ignored since the mapper would not extend the target class.
Want to make some tests, but cannot find related examples in this repository. Still looking around to search for some good examples.
Notes:
-
Since this plugin builds against IntelliJ IDEA 2020.1 which does not bundle the Lombok plugin, the dependency declaration should specify a version of it. I've used
0.34-2020.1and0.34-2020.2which is the latest version respectively for the CI. Configurable by set environment variableLOMBOK_PLUGIN_VERSION.By the way, I thought the plugin is bundled since IntelliJ IDEA 2020.3 but it seems broken. It succeeded with 2020.3.4 but not with 2020.3, so in this case, I just set it to
0.32-EAPwhich is the only version supports IntelliJ IDEA 2020.3. Please let me know if you mind. (But I don't know the clear solution until now 😉)Note that 'Lombook' is not a typo, the plugin id really states.
-
Like Kotlin, the Lombok plugin is optional. The dependency is only for the
instanceofcheck, so even no extra configuration is needed. (But the file is added since the platform requires existence.) -
IntelliJ Platform Plugin documentation guides to name the additional plugin descriptor files as "myPluginId-$NAME$.xml" so I've followed the standard.
Reference: https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html#optional-plugin-dependencies
-
BONUS: Fixed plugin repository in README, added
ideaVersion2021.3 to the CI matrix.
@filiphr Can I get some review?
Hey @KENNYSOFT, thanks for the PR, I've been a bit busy and haven't had time to look into it yet. Will look into it once I have some time. On first glance I do not like the required dependency on the Lombok plugin, we are importing LombokLightMethodBuilder, which means that we can't run when that plugin is not available.
@KENNYSOFT what happens if the Lombok plugin is not available? I see that LombokLightMethodBuilder is being used, but if the plugin is not there, I guess that the class won't be there as well.
OK, I thought the main concerns about the class LombokLightMethodBuilder are on two sides:
-
import de.plushnikov.intellij.plugin.psi.LombokLightMethodBuilder; -
if ( constructor instanceof LombokLightMethodBuilder ) {
I've tested with the (bundled) Lombok plugin disabled, it works without any crashes.
- I don't know what's the magic, but importing not existing class does not affect the IDE. It does not make any error logs. (Maybe something similar to Spring's
@ConditionalOnClass?) I can add some try-catch around to there and useClass.forName(), would you mind this solution? - In my test case, the IDE cannot access the
instanceofexpression, since it was blocked from:
Especially,if ( constructor != null && constructor.hasParameters() ) {constructorwasnullas I only used Lombok to make a constructor.
I'll soon attach the Minimal Reproducible Example. Still do not have any idea about the test code.
Minimal Reproducible Example
build.gradle
plugins {
id 'java'
id 'io.freefair.lombok' version '8.0.1'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.mapstruct:mapstruct:1.5.4.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.4.Final'
}
Content.java
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
public class Content {
private Long no;
private String title;
private String description;
private LocalDateTime createdAt;
}
ContentCreateRequest.java
public record ContentCreateRequest(String title, String description) {
}
ContentRequestMapper.java
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
@Mapper(unmappedSourcePolicy = ReportingPolicy.ERROR, unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface ContentRequestMapper {
@Mapping(target = "no", expression = "java(null)")
@Mapping(target = "createdAt", expression = "java(null)")
Content toEntity(ContentCreateRequest request);
}
Result
With Lombok extension:

Without:

I've tested with the (bundled) Lombok plugin disabled, it works without any crashes.
Perhaps it works because the plugin is bundled and thus the class is always there. I'll try it out and see what happens. My main concern was that there will be some kind of an exception when we do the instance of check or when the IDE boots with the MapStruct plugin.
@KENNYSOFT I am not sure how you tested this, but when I try this out with the Lombok plugin disabled on IntelliJ IDEA 2023.1.1 Preview (Ultimate Edition)
then I get:
java.lang.NoClassDefFoundError: de/plushnikov/intellij/plugin/psi/LombokLightMethodBuilder
at org.mapstruct.intellij.codeinsight.references.MapstructTargetReference.resolveInternal(MapstructTargetReference.java:90)
at org.mapstruct.intellij.codeinsight.references.MapstructTargetReference.resolveInternal(MapstructTargetReference.java:134)
at org.mapstruct.intellij.codeinsight.references.MapstructBaseReference.resolve(MapstructBaseReference.java:76)
at org.mapstruct.intellij.expression.JavaExpressionInjector.getLanguagesToInject(JavaExpressionInjector.java:178)
at com.intellij.psi.impl.source.tree.injected.InjectedLanguageManagerImpl.processInPlaceInjectorsFor(InjectedLanguageManagerImpl.java:445)
at com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtilBase.probeElementsUpInner(InjectedLanguageUtilBase.java:238)
at com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtilBase.lambda$probeElementsUp$0(InjectedLanguageUtilBase.java:218)
at com.intellij.openapi.application.impl.ReadActionCacheIml$allowInWriteAction$1.invoke(ReadActionCacheIml.kt:22)
at com.intellij.openapi.application.impl.ReadActionCacheIml$allowInWriteAction$1.invoke(ReadActionCacheIml.kt:22)
at com.intellij.openapi.application.impl.ReadActionCacheIml.allowInWriteAction$lambda$0(ReadActionCacheIml.kt:18)
at com.intellij.openapi.application.impl.ReadMostlyRWLock.allowProcessingContextInWriteAction(ReadMostlyRWLock.java:204)
at com.intellij.openapi.application.impl.ReadActionCacheIml.allowInWriteAction(ReadActionCacheIml.kt:18)
at com.intellij.openapi.application.impl.ReadActionCacheIml.allowInWriteAction(ReadActionCacheIml.kt:22)
at com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtilBase.probeElementsUp(InjectedLanguageUtilBase.java:217)
at com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtilBase.enumerate(InjectedLanguageUtilBase.java:159)
at com.intellij.psi.impl.source.tree.injected.InjectedLanguageManagerImpl.enumerateEx(InjectedLanguageManagerImpl.java:329)
at com.intellij.codeInsight.daemon.impl.LineMarkersPass.queryLineMarkersForInjected(LineMarkersPass.java:251)
at com.intellij.codeInsight.daemon.impl.LineMarkersPass.queryProviders(LineMarkersPass.java:214)
at com.intellij.codeInsight.daemon.impl.LineMarkersPass.lambda$doCollectInformation$3(LineMarkersPass.java:104)
at com.intellij.codeInsight.daemon.impl.Divider.divideInsideAndOutsideInOneRoot(Divider.java:95)
at com.intellij.codeInsight.daemon.impl.LineMarkersPass.doCollectInformation(LineMarkersPass.java:99)
at com.intellij.codeHighlighting.TextEditorHighlightingPass.collectInformation(TextEditorHighlightingPass.java:57)
at com.intellij.codeInsight.daemon.impl.PassExecutorService$ScheduledPass.lambda$doRun$1(PassExecutorService.java:382)
at com.intellij.openapi.application.impl.ApplicationImpl.tryRunReadAction(ApplicationImpl.java:1102)
at com.intellij.codeInsight.daemon.impl.PassExecutorService$ScheduledPass.lambda$doRun$2(PassExecutorService.java:374)
at com.intellij.openapi.progress.impl.CoreProgressManager.lambda$executeProcessUnderProgress$13(CoreProgressManager.java:604)
at com.intellij.openapi.progress.impl.CoreProgressManager.registerIndicatorAndRun(CoreProgressManager.java:679)
at com.intellij.openapi.progress.impl.CoreProgressManager.computeUnderProgress(CoreProgressManager.java:635)
at com.intellij.openapi.progress.impl.CoreProgressManager.executeProcessUnderProgress(CoreProgressManager.java:603)
at com.intellij.openapi.progress.impl.ProgressManagerImpl.executeProcessUnderProgress(ProgressManagerImpl.java:60)
at com.intellij.codeInsight.daemon.impl.PassExecutorService$ScheduledPass.doRun(PassExecutorService.java:373)
at com.intellij.codeInsight.daemon.impl.PassExecutorService$ScheduledPass.lambda$run$0(PassExecutorService.java:349)
at com.intellij.openapi.application.impl.ReadMostlyRWLock.executeByImpatientReader(ReadMostlyRWLock.java:229)
at com.intellij.openapi.application.impl.ApplicationImpl.executeByImpatientReader(ApplicationImpl.java:187)
at com.intellij.codeInsight.daemon.impl.PassExecutorService$ScheduledPass.run(PassExecutorService.java:347)
at com.intellij.concurrency.JobLauncherImpl$VoidForkJoinTask$1.exec(JobLauncherImpl.java:181)
at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373)
at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182)
at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655)
at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622)
at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165)
Caused by: java.lang.ClassNotFoundException: de.plushnikov.intellij.plugin.psi.LombokLightMethodBuilder PluginClassLoader(plugin=PluginDescriptor(name=MapStruct Support, id=org.mapstruct.intellij, descriptorPath=plugin.xml, path=~/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/231.8770.17/IntelliJ IDEA.app.plugins/MapStruct-Intellij-Plugin, version=1.5.1-SNAPSHOT, package=null, isBundled=false), packagePrefix=null, instanceId=316, state=active)
at com.intellij.ide.plugins.cl.PluginClassLoader.loadClass(PluginClassLoader.kt:150)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
... 41 more
However, I think that I found a nice and neat way to support this. Have a look at my commit. I also adapted some other places in order to properly go to the field. Let me know what you think
However, I think that I found a nice and neat way to support this. Have a look at my commit. I also adapted some other places in order to properly go to the field. Let me know what you think
Yeah, cool for extracting, but I cannot find that what behavior is changed by resolvePsiElementForMethod. It seems that LightLombokMethod is automatically linked and resolved to the field when accessing getter/setter/builder methods.
TestDtoOnlyGetter.java
import lombok.Getter;
@Getter
public class TestDtoOnlyGetter {
private String nameg;
}
TestDtoWithBuilder.java
import lombok.Builder;
@Builder
public class TestDtoWithBuilder {
private String nameb;
}
TestDtoWithSetter.java
import lombok.Setter;
@Setter
public class TestDtoWithSetter {
private String names;
}
TestMapper.java
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper
public interface TestMapper {
@Mapping(target = "nameb", source = "name")
TestDtoWithBuilder resolveTargetBuilder(String name);
@Mapping(target = "names", source = "name")
TestDtoWithSetter resolveTargetSetter(String name);
@Mapping(target = "names", source = "dto.nameg")
TestDtoWithSetter resolveSourceGetter(TestDtoOnlyGetter dto);
}
nameg, nameb, names can all be referenced in the @Mapping target and source, even with the current GA version of the plugin.
Good point @KENNYSOFT. I was testing this initially with IntelliJ 2021.2 and there it didn't work. It only started working when I did that resolvement. However, I tried it now on 2023.1.1 Preview and everything works properly. Even when the parameter comes from the constructor.
What exactly were your initial changes fixing @KENNYSOFT?