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

Add support for Lombok #92

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

KENNYSOFT
Copy link
Contributor

@KENNYSOFT KENNYSOFT commented Feb 14, 2022

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.1 and 0.34-2020.2 which is the latest version respectively for the CI. Configurable by set environment variable LOMBOK_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-EAP which 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 instanceof check, 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 ideaVersion 2021.3 to the CI matrix.

@KENNYSOFT KENNYSOFT changed the title Feature/lombok Add support for Lombok Feb 14, 2022
@KENNYSOFT
Copy link
Contributor Author

@filiphr Can I get some review?

@filiphr
Copy link
Member

filiphr commented Feb 27, 2022

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.

@filiphr
Copy link
Member

filiphr commented Apr 22, 2023

@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.

@KENNYSOFT
Copy link
Contributor Author

OK, I thought the main concerns about the class LombokLightMethodBuilder are on two sides:

  1. import de.plushnikov.intellij.plugin.psi.LombokLightMethodBuilder;
  2. if ( constructor instanceof LombokLightMethodBuilder ) {

I've tested with the (bundled) Lombok plugin disabled, it works without any crashes.

  1. 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 use Class.forName(), would you mind this solution?
  2. In my test case, the IDE cannot access the instanceof expression, since it was blocked from:
    if ( constructor != null && constructor.hasParameters() ) {
    Especially, constructor was null as 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.

@KENNYSOFT
Copy link
Contributor Author

KENNYSOFT commented Apr 23, 2023

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:

image

Without:

image

@filiphr
Copy link
Member

filiphr commented Apr 23, 2023

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.

@filiphr
Copy link
Member

filiphr commented Apr 23, 2023

@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

@KENNYSOFT
Copy link
Contributor Author

KENNYSOFT commented Apr 26, 2023

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.

@filiphr
Copy link
Member

filiphr commented Apr 26, 2023

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants