diff --git a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/codeAction/proposal/RemoveMethodProposal.java b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/codeAction/proposal/RemoveMethodProposal.java new file mode 100644 index 000000000..7e2694ff3 --- /dev/null +++ b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/codeAction/proposal/RemoveMethodProposal.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2026 IBM Corporation and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ + +package io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.codeAction.proposal; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiModifierListOwner; + +import java.util.List; + +/** + * Code action proposal for removing methods from a class. + */ +public class RemoveMethodProposal extends RemoveElementsProposal { + + /** + * Constructor for RemoveMethodProposal. + * + * @param label The label for this proposal + * @param sourceCU The source compilation unit + * @param invocationNode The invocation node + * @param binding The modifier list owner (the class) + * @param relevance The relevance score + * @param methodsToRemove List of methods to remove + */ + public RemoveMethodProposal( + String label, + PsiFile sourceCU, + PsiFile invocationNode, + PsiModifierListOwner binding, + int relevance, + List methodsToRemove) { + super(label, sourceCU, invocationNode, binding, relevance, methodsToRemove); + } +} \ No newline at end of file diff --git a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/ejb/EjbConstants.java b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/ejb/EjbConstants.java index 12b5d2b8f..0afc7e1c0 100644 --- a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/ejb/EjbConstants.java +++ b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/ejb/EjbConstants.java @@ -29,4 +29,7 @@ public class EjbConstants { public static final String DIAGNOSTIC_SOURCE = "jakarta-ejb"; public static final String DIAGNOSTIC_CODE_MISSING_PUBLIC_CONSTRUCTOR = "MissingPublicNoArgConstructor"; + public static final String DIAGNOSTIC_CODE_FINALIZE_METHOD = "SessionBeanFinalizeMethod"; + + public static final String FINALIZE_METHOD_NAME = "finalize"; } \ No newline at end of file diff --git a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/ejb/EjbDiagnosticsCollector.java b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/ejb/EjbDiagnosticsCollector.java index 5a37a5854..66bfce6a6 100644 --- a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/ejb/EjbDiagnosticsCollector.java +++ b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/ejb/EjbDiagnosticsCollector.java @@ -52,6 +52,7 @@ public void collectDiagnostics(PsiJavaFile unit, List diagnostics) { if (!sessionBeanAnnotations.isEmpty()) { validateSessionBeanConstructor(type, unit, diagnostics); + validateSessionBeanFinalizeMethod(type, unit, diagnostics); } } } @@ -98,4 +99,31 @@ private void validateSessionBeanConstructor(PsiClass type, PsiJavaFile unit, Lis DiagnosticSeverity.Error)); } } + + /** + * Validates that a session bean does not define or override the finalize() method. + * + * According to the Jakarta EE Enterprise Beans specification, session bean classes + * must not override or define the finalize() method. The container manages the + * lifecycle and cleanup of session beans. + * + * @param type the class to validate + * @param unit the compilation unit + * @param diagnostics the list to add diagnostics to + */ + private void validateSessionBeanFinalizeMethod(PsiClass type, PsiJavaFile unit, List diagnostics) { + // Check all methods in the class + for (PsiMethod method : type.getMethods()) { + // Check if this is the finalize() method + if (FINALIZE_METHOD_NAME.equals(method.getName()) && + method.getParameterList().getParametersCount() == 0) { + // Report diagnostic for finalize() method + diagnostics.add(createDiagnostic(method, unit, + Messages.getMessage("SessionBeanFinalizeMethod"), + DIAGNOSTIC_CODE_FINALIZE_METHOD, + null, + DiagnosticSeverity.Error)); + } + } + } } \ No newline at end of file diff --git a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/ejb/RemoveFinalizeMethodQuickFix.java b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/ejb/RemoveFinalizeMethodQuickFix.java new file mode 100644 index 000000000..a77e877bb --- /dev/null +++ b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/ejb/RemoveFinalizeMethodQuickFix.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2026 IBM Corporation and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ + +package io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.ejb; + +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.util.PsiTreeUtil; +import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.JDTUtils; +import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.Messages; +import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.codeAction.proposal.RemoveMethodProposal; + +import static io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.ejb.EjbConstants.FINALIZE_METHOD_NAME; +import io.openliberty.tools.intellij.lsp4mp4ij.psi.core.java.codeaction.IJavaCodeActionParticipant; +import io.openliberty.tools.intellij.lsp4mp4ij.psi.core.java.codeaction.JavaCodeActionContext; +import io.openliberty.tools.intellij.lsp4mp4ij.psi.core.java.codeaction.JavaCodeActionResolveContext; +import io.openliberty.tools.intellij.lsp4mp4ij.psi.core.java.corrections.proposal.ChangeCorrectionProposal; +import io.openliberty.tools.intellij.util.ExceptionUtil; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.Diagnostic; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +/** + * QuickFix for removing the finalize() method from session beans. + */ +public class RemoveFinalizeMethodQuickFix implements IJavaCodeActionParticipant { + + private static final String NAME = Messages.getMessage("RemoveFinalizeMethod"); + private static final Logger LOGGER = Logger.getLogger(RemoveFinalizeMethodQuickFix.class.getName()); + + @Override + public String getParticipantId() { + return RemoveFinalizeMethodQuickFix.class.getName(); + } + + @Override + public List getCodeActions(JavaCodeActionContext context, Diagnostic diagnostic) { + List codeActions = new ArrayList<>(); + final PsiElement node = context.getCoveredNode(); + final PsiMethod parentMethod = PsiTreeUtil.getParentOfType(node, PsiMethod.class); + + if (parentMethod != null && FINALIZE_METHOD_NAME.equals(parentMethod.getName()) && + parentMethod.getParameterList().getParametersCount() == 0) { + codeActions.add(JDTUtils.createCodeAction(context, diagnostic, NAME, getParticipantId())); + } + return codeActions; + } + + @Override + public CodeAction resolveCodeAction(JavaCodeActionResolveContext context) { + final CodeAction toResolve = context.getUnresolved(); + final PsiElement node = context.getCoveredNode(); + final PsiClass parentType = PsiTreeUtil.getParentOfType(node, PsiClass.class); + final PsiMethod parentMethod = PsiTreeUtil.getParentOfType(node, PsiMethod.class); + + if (parentMethod != null && FINALIZE_METHOD_NAME.equals(parentMethod.getName())) { + ChangeCorrectionProposal proposal = new RemoveMethodProposal(NAME, + context.getSource().getCompilationUnit(), + context.getASTRoot(), + parentType, + 0, + Collections.singletonList(parentMethod)); + + ExceptionUtil.executeWithWorkspaceEditHandling(context, proposal, toResolve, LOGGER, + "Unable to create workspace edit for removing finalize() method"); + } + return toResolve; + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 5bb7579b3..ed5ddc40e 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -623,6 +623,10 @@ group="jakarta" targetDiagnostic="jakarta-ejb#MissingPublicNoArgConstructor" implementationClass="io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.ejb.InsertPublicNoArgConstructorQuickFix"/> + diff --git a/src/main/resources/io/openliberty/tools/intellij/lsp4jakarta/messages/messages.properties b/src/main/resources/io/openliberty/tools/intellij/lsp4jakarta/messages/messages.properties index 8bae3d634..a8b002109 100644 --- a/src/main/resources/io/openliberty/tools/intellij/lsp4jakarta/messages/messages.properties +++ b/src/main/resources/io/openliberty/tools/intellij/lsp4jakarta/messages/messages.properties @@ -271,6 +271,10 @@ InvalidMultipleInterceptorMethodsOfSameType = Only one method with {0} annotatio # EjbDiagnosticsCollector SessionBeanNoArgConstructor = Session beans must have a public no-arg constructor. +SessionBeanFinalizeMethod = Session bean classes must not override or define the finalize() method. # InsertPublicNoArgConstructorQuickFix AddPublicNoArgConstructor = Add a no-arg public constructor to this class + +# RemoveFinalizeMethodQuickFix +RemoveFinalizeMethod = Remove the finalize() method diff --git a/src/test/java/io/openliberty/tools/intellij/lsp4jakarta/it/ejb/SessionBeanFinalizeTest.java b/src/test/java/io/openliberty/tools/intellij/lsp4jakarta/it/ejb/SessionBeanFinalizeTest.java new file mode 100644 index 000000000..96c1f23b6 --- /dev/null +++ b/src/test/java/io/openliberty/tools/intellij/lsp4jakarta/it/ejb/SessionBeanFinalizeTest.java @@ -0,0 +1,171 @@ +/******************************************************************************* +* Copyright (c) 2026 IBM Corporation and others. +* +* This program and the accompanying materials are made available under the +* terms of the Eclipse Public License v. 2.0 which is available at +* http://www.eclipse.org/legal/epl-2.0. +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* IBM Corporation - initial API and implementation +*******************************************************************************/ + +package io.openliberty.tools.intellij.lsp4jakarta.it.ejb; + +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleUtilCore; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import io.openliberty.tools.intellij.lsp4jakarta.it.core.BaseJakartaTest; +import io.openliberty.tools.intellij.lsp4mp4ij.psi.core.utils.IPsiUtils; +import io.openliberty.tools.intellij.lsp4mp4ij.psi.internal.core.ls.PsiUtilsLSImpl; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.DiagnosticSeverity; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4jakarta.commons.JakartaJavaCodeActionParams; +import org.eclipse.lsp4jakarta.commons.JakartaJavaDiagnosticsParams; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.util.Arrays; + +import static io.openliberty.tools.intellij.lsp4jakarta.it.core.JakartaForJavaAssert.*; + +@RunWith(JUnit4.class) +public class SessionBeanFinalizeTest extends BaseJakartaTest { + + @Test + public void testInvalidStatelessBeanFinalize() throws Exception { + Module module = createMavenModule(new File("src/test/resources/projects/maven/jakarta-sample")); + IPsiUtils utils = PsiUtilsLSImpl.getInstance(getProject()); + + VirtualFile javaFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(ModuleUtilCore.getModuleDirPath(module) + + "/src/main/java/io/openliberty/sample/jakarta/ejb/InvalidStatelessBeanFinalize.java"); + String uri = VfsUtilCore.virtualToIoFile(javaFile).toURI().toString(); + + JakartaJavaDiagnosticsParams diagnosticsParams = new JakartaJavaDiagnosticsParams(); + diagnosticsParams.setUris(Arrays.asList(uri)); + + // Test expected diagnostic + Diagnostic expectedDiagnostic = d(18, 19, 27, + "Session bean classes must not override or define the finalize() method.", + DiagnosticSeverity.Error, "jakarta-ejb", "SessionBeanFinalizeMethod"); + + assertJavaDiagnostics(diagnosticsParams, utils, expectedDiagnostic); + + // Test expected quick-fix + JakartaJavaCodeActionParams codeActionParams = createCodeActionParams(uri, expectedDiagnostic); + String newText = "package io.openliberty.sample.jakarta.ejb;" + + "\n\nimport jakarta.ejb.Stateless;" + + "\n\n@Stateless" + + "\npublic class InvalidStatelessBeanFinalize {" + + "\n private String data;" + + "\n\n public String getData() {" + + "\n return data;" + + "\n }" + + "\n\n public void setData(String data) {" + + "\n this.data = data;" + + "\n }" + + "\n\n}"; + TextEdit expectedTextEdit = te(0, 0, 22, 1, newText); + CodeAction expectedCodeAction = ca(uri, "Remove the finalize() method", expectedDiagnostic, expectedTextEdit); + assertJavaCodeAction(codeActionParams, utils, expectedCodeAction); + } + + @Test + public void testInvalidStatefulBeanFinalize() throws Exception { + Module module = createMavenModule(new File("src/test/resources/projects/maven/jakarta-sample")); + IPsiUtils utils = PsiUtilsLSImpl.getInstance(getProject()); + + VirtualFile javaFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(ModuleUtilCore.getModuleDirPath(module) + + "/src/main/java/io/openliberty/sample/jakarta/ejb/InvalidStatefulBeanFinalize.java"); + String uri = VfsUtilCore.virtualToIoFile(javaFile).toURI().toString(); + + JakartaJavaDiagnosticsParams diagnosticsParams = new JakartaJavaDiagnosticsParams(); + diagnosticsParams.setUris(Arrays.asList(uri)); + + // Test expected diagnostic + Diagnostic expectedDiagnostic = d(18, 19, 27, + "Session bean classes must not override or define the finalize() method.", + DiagnosticSeverity.Error, "jakarta-ejb", "SessionBeanFinalizeMethod"); + + assertJavaDiagnostics(diagnosticsParams, utils, expectedDiagnostic); + + // Test expected quick-fix + JakartaJavaCodeActionParams codeActionParams = createCodeActionParams(uri, expectedDiagnostic); + String newText = "package io.openliberty.sample.jakarta.ejb;" + + "\n\nimport jakarta.ejb.Stateful;" + + "\n\n@Stateful" + + "\npublic class InvalidStatefulBeanFinalize {" + + "\n private int count;" + + "\n\n public int getCount() {" + + "\n return count;" + + "\n }" + + "\n\n public void setCount(int count) {" + + "\n this.count = count;" + + "\n }" + + "\n\n}"; + TextEdit expectedTextEdit = te(0, 0, 22, 1, newText); + CodeAction expectedCodeAction = ca(uri, "Remove the finalize() method", expectedDiagnostic, expectedTextEdit); + assertJavaCodeAction(codeActionParams, utils, expectedCodeAction); + } + + @Test + public void testInvalidSingletonBeanFinalize() throws Exception { + Module module = createMavenModule(new File("src/test/resources/projects/maven/jakarta-sample")); + IPsiUtils utils = PsiUtilsLSImpl.getInstance(getProject()); + + VirtualFile javaFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(ModuleUtilCore.getModuleDirPath(module) + + "/src/main/java/io/openliberty/sample/jakarta/ejb/InvalidSingletonBeanFinalize.java"); + String uri = VfsUtilCore.virtualToIoFile(javaFile).toURI().toString(); + + JakartaJavaDiagnosticsParams diagnosticsParams = new JakartaJavaDiagnosticsParams(); + diagnosticsParams.setUris(Arrays.asList(uri)); + + // Test expected diagnostic + Diagnostic expectedDiagnostic = d(18, 19, 27, + "Session bean classes must not override or define the finalize() method.", + DiagnosticSeverity.Error, "jakarta-ejb", "SessionBeanFinalizeMethod"); + + assertJavaDiagnostics(diagnosticsParams, utils, expectedDiagnostic); + + // Test expected quick-fix + JakartaJavaCodeActionParams codeActionParams = createCodeActionParams(uri, expectedDiagnostic); + String newText = "package io.openliberty.sample.jakarta.ejb;" + + "\n\nimport jakarta.ejb.Singleton;" + + "\n\n@Singleton" + + "\npublic class InvalidSingletonBeanFinalize {" + + "\n private String config;" + + "\n\n public String getConfig() {" + + "\n return config;" + + "\n }" + + "\n\n public void setConfig(String config) {" + + "\n this.config = config;" + + "\n }" + + "\n\n}"; + TextEdit expectedTextEdit = te(0, 0, 22, 1, newText); + CodeAction expectedCodeAction = ca(uri, "Remove the finalize() method", expectedDiagnostic, expectedTextEdit); + assertJavaCodeAction(codeActionParams, utils, expectedCodeAction); + } + + @Test + public void testValidStatelessBeanNoFinalize() throws Exception { + Module module = createMavenModule(new File("src/test/resources/projects/maven/jakarta-sample")); + IPsiUtils utils = PsiUtilsLSImpl.getInstance(getProject()); + + VirtualFile javaFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(ModuleUtilCore.getModuleDirPath(module) + + "/src/main/java/io/openliberty/sample/jakarta/ejb/ValidStatelessBeanNoFinalize.java"); + String uri = VfsUtilCore.virtualToIoFile(javaFile).toURI().toString(); + + JakartaJavaDiagnosticsParams diagnosticsParams = new JakartaJavaDiagnosticsParams(); + diagnosticsParams.setUris(Arrays.asList(uri)); + + // Should not report any diagnostics for valid bean without finalize() + assertJavaDiagnostics(diagnosticsParams, utils); + } +} \ No newline at end of file diff --git a/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/ejb/InvalidSingletonBeanFinalize.java b/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/ejb/InvalidSingletonBeanFinalize.java new file mode 100644 index 000000000..549f0c9ab --- /dev/null +++ b/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/ejb/InvalidSingletonBeanFinalize.java @@ -0,0 +1,23 @@ +package io.openliberty.sample.jakarta.ejb; + +import jakarta.ejb.Singleton; + +@Singleton +public class InvalidSingletonBeanFinalize { + private String config; + + public String getConfig() { + return config; + } + + public void setConfig(String config) { + this.config = config; + } + + // Session beans must not define finalize() method + @Override + protected void finalize() throws Throwable { + // Cleanup code - this violates EJB specification + config = null; + } +} \ No newline at end of file diff --git a/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/ejb/InvalidStatefulBeanFinalize.java b/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/ejb/InvalidStatefulBeanFinalize.java new file mode 100644 index 000000000..02a806493 --- /dev/null +++ b/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/ejb/InvalidStatefulBeanFinalize.java @@ -0,0 +1,23 @@ +package io.openliberty.sample.jakarta.ejb; + +import jakarta.ejb.Stateful; + +@Stateful +public class InvalidStatefulBeanFinalize { + private int count; + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + // Session beans must not define finalize() method + @Override + protected void finalize() throws Throwable { + // Cleanup code - this violates EJB specification + count = 0; + } +} \ No newline at end of file diff --git a/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/ejb/InvalidStatelessBeanFinalize.java b/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/ejb/InvalidStatelessBeanFinalize.java new file mode 100644 index 000000000..238a05051 --- /dev/null +++ b/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/ejb/InvalidStatelessBeanFinalize.java @@ -0,0 +1,23 @@ +package io.openliberty.sample.jakarta.ejb; + +import jakarta.ejb.Stateless; + +@Stateless +public class InvalidStatelessBeanFinalize { + private String data; + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + // Session beans must not define finalize() method + @Override + protected void finalize() throws Throwable { + // Cleanup code - this violates EJB specification + data = null; + } +} \ No newline at end of file diff --git a/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/ejb/ValidStatelessBeanNoFinalize.java b/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/ejb/ValidStatelessBeanNoFinalize.java new file mode 100644 index 000000000..876abadb9 --- /dev/null +++ b/src/test/resources/projects/maven/jakarta-sample/src/main/java/io/openliberty/sample/jakarta/ejb/ValidStatelessBeanNoFinalize.java @@ -0,0 +1,22 @@ +package io.openliberty.sample.jakarta.ejb; + +import jakarta.ejb.Stateless; + +@Stateless +public class ValidStatelessBeanNoFinalize { + private String data; + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + // This is valid - no finalize() method defined + public void cleanup() { + // Custom cleanup method is fine + data = null; + } +} \ No newline at end of file