diff --git a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/cdi/ManagedBeanConstants.java b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/cdi/ManagedBeanConstants.java index b388e7f80..9f3e6e427 100644 --- a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/cdi/ManagedBeanConstants.java +++ b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/cdi/ManagedBeanConstants.java @@ -30,6 +30,7 @@ public class ManagedBeanConstants { public static final String SINGLETON_FQ_NAME = "jakarta.ejb.Singleton"; public static final String APPLICATION_SCOPED_FQ_NAME = "jakarta.enterprise.context.ApplicationScoped"; public static final String STATELESS_FQ_NAME = "jakarta.ejb.Stateless"; + public static final String NORMAL_SCOPE_FQ_NAME = "jakarta.enterprise.context.NormalScope"; public static final String NAMED_FQ_NAME = "jakarta.inject.Named"; public static final String DIAGNOSTIC_SOURCE = "jakarta-cdi"; @@ -50,6 +51,7 @@ public class ManagedBeanConstants { public static final String DIAGNOSTIC_CODE_INTERCEPTOR_DECORATOR_OBSERVER = "InvalidInterceptorOrDecoratorWithObserverMethod"; public static final String DIAGNOSTIC_CODE_DEPENDENT_CONDITIONAL_OBSERVER = "InvalidDependentScopeWithConditionalObserver"; public static final String DIAGNOSTIC_MULTIPLE_OBSERVER_PARAMS = "InvalidMultipleObserverParams"; + public static final String DIAGNOSTIC_CODE_INTERCEPTOR_DECORATOR_ILLEGAL_SCOPE = "InvalidInterceptorOrDecorator"; public static final String DIAGNOSTIC_CODE_REDUNDANT_DISPOSES = "RemoveExtraDisposes"; //Added as part of fix that adds two quick fixes which are mutually exclusive issue #540 public static final String[] INVALID_DISPOSER_FQ_PARAMS = { DISPOSES_FQ_NAME }; @@ -65,5 +67,13 @@ public class ManagedBeanConstants { "jakarta.enterprise.context.SessionScoped", "jakarta.enterprise.context.NormalScope", "jakarta.Interceptor", "jakarta.Decorator", "jakarta.enterprise.inject.Stereotype")); + // Scopes that are invalid for interceptors and decorators (they must use @Dependent only) + public static final String[] INVALID_INTERCEPTOR_DECORATOR_SCOPES = { + "jakarta.enterprise.context.ApplicationScoped", + "jakarta.enterprise.context.SessionScoped", + "jakarta.enterprise.context.ConversationScoped", + "jakarta.enterprise.context.RequestScoped" + }; + public static final Set INVALID_OBSERVES_OBSERVES_ASYNC_CONFLICTED_PARAMS = Set.of(OBSERVES_FQ_NAME, OBSERVES_ASYNC_FQ_NAME); } \ No newline at end of file diff --git a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/cdi/ManagedBeanDiagnosticsCollector.java b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/cdi/ManagedBeanDiagnosticsCollector.java index def2867cb..21315e8d4 100644 --- a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/cdi/ManagedBeanDiagnosticsCollector.java +++ b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/cdi/ManagedBeanDiagnosticsCollector.java @@ -16,6 +16,7 @@ import java.util.*; import java.util.stream.Stream; +import com.intellij.codeInsight.AnnotationUtil; import com.intellij.psi.*; import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.AbstractDiagnosticsCollector; import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.Messages; @@ -336,7 +337,18 @@ DIAGNOSTIC_CODE_SCOPEDECL, new Gson().toJsonTree(managedBeanAnnotations), */ invalidParamsCheck(unit, diagnostics, type, INJECT_FQ_NAME, ManagedBeanConstants.DIAGNOSTIC_CODE_INVALID_INJECT_PARAM); - + // Interceptors and decorators must not have normal scopes (ApplicationScoped, SessionScoped, etc.) + // They should only use @Dependent scope + if (interceptorOrDecorator) { + List foundInvalidScopes = validateInterceptorDecoratorScopes(type, typeAnnotations); + if (!foundInvalidScopes.isEmpty()) { + diagnostics.add(createDiagnostic(type, unit, + Messages.getMessage("InterceptorOrDecoratorWithIllegalScope"), + DIAGNOSTIC_CODE_INTERCEPTOR_DECORATOR_ILLEGAL_SCOPE, + new Gson().toJsonTree(foundInvalidScopes), + DiagnosticSeverity.Error)); + } + } if (isManagedBean) { /* * ========= Produces and Disposes, Observes, ObservesAsync Annotations Checks========= @@ -529,4 +541,51 @@ private boolean hasConditionalObserverAnnotation(PsiClass type, PsiMethod method .flatMap(param -> Stream.of(param.getAnnotations())) .anyMatch(annotation -> isConditionalObserver(type, annotation)); } + + /** + * validateInterceptorDecoratorScopes + * Validates that interceptors and decorators do not declare invalid scope annotations. + * Interceptors and decorators must not have normal scopes (ApplicationScoped, SessionScoped, etc.) + * and should only use @Dependent scope. Detects both built-in CDI scopes and custom @NormalScope annotations. + * + * @param type the Java type being validated + * @param typeAnnotations the annotations on the type + * @return list of invalid scope annotation fully qualified names + */ + private List validateInterceptorDecoratorScopes(PsiClass type, PsiAnnotation[] typeAnnotations) { + List foundInvalidScopes = new ArrayList<>(); + + // Check each annotation to see if it's an invalid scope + for (PsiAnnotation annotation : typeAnnotations) { + String annotationName = annotation.getQualifiedName(); + // Skip @Interceptor, @Decorator, and @Dependent annotations - these are not scopes we're checking + String matchedSkip = getMatchedJavaElementName(type, annotationName, + new String[]{ + INTERCEPTOR_FQ_NAME, + DECORATOR_FQ_NAME, + DEPENDENT_FQ_NAME + }); + if (matchedSkip != null) { + continue; + } + // Check if it's a built-in invalid scope + String matchedBuiltInScope = getMatchedJavaElementName(type, annotationName, + INVALID_INTERCEPTOR_DECORATOR_SCOPES); + if (matchedBuiltInScope != null) { + foundInvalidScopes.add(matchedBuiltInScope); + } else { + // Check if it's a custom @NormalScope annotation using AnnotationUtil + try { + PsiClass annotationType = JavaPsiFacade.getInstance(type.getProject()) + .findClass(annotationName, type.getResolveScope()); + if (annotationType != null && AnnotationUtil.isAnnotated(annotationType, NORMAL_SCOPE_FQ_NAME, 0)) { + foundInvalidScopes.add(annotationName); + } + } catch (Exception e) { + // Ignore exceptions during annotation type resolution + } + } + } + return foundInvalidScopes; + } } \ No newline at end of file diff --git a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/cdi/ReplaceInvalidScopesWithDependentQuickFix.java b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/cdi/ReplaceInvalidScopesWithDependentQuickFix.java new file mode 100644 index 000000000..670fac355 --- /dev/null +++ b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/cdi/ReplaceInvalidScopesWithDependentQuickFix.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * 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 implementation + *******************************************************************************/ +package io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.cdi; + +import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.Messages; +import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.codeAction.proposal.quickfix.ReplaceAnnotationsQuickFix; + +import java.util.List; +import java.util.stream.Collectors; + +import static io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.JDTUtils.getSimpleName; + +/** + * Quickfix for InvalidInterceptorOrDecorator diagnostic. + * Replaces all invalid scope annotations with @Dependent. + */ +public class ReplaceInvalidScopesWithDependentQuickFix extends ReplaceAnnotationsQuickFix { + + /** + * Constructor. + */ + public ReplaceInvalidScopesWithDependentQuickFix() { + super(ManagedBeanConstants.DEPENDENT_FQ_NAME); + } + + /** + * {@inheritDoc} + */ + @Override + public String getParticipantId() { + return ReplaceInvalidScopesWithDependentQuickFix.class.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + protected String getCodeActionLabel(List annotationsToRemove) { + String formattedNames = formatAnnotationNames(annotationsToRemove); + return Messages.getMessage("ReplaceInvalidScopesWithDependent", formattedNames); + } + + /** + * Formats a list of fully qualified annotation names for display. + * Extracts simple names and joins them with commas, prefixed with @. + * + * @param annotationFqNames List of fully qualified annotation names + * @return Formatted string (e.g., "@ApplicationScoped, @RequestScoped") + */ + private String formatAnnotationNames(List annotationFqNames) { + return annotationFqNames.stream() + .map(fqName -> "@" + getSimpleName(fqName)) + .collect(Collectors.joining(", ")); + } +} + diff --git a/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/codeAction/proposal/quickfix/ReplaceAnnotationsQuickFix.java b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/codeAction/proposal/quickfix/ReplaceAnnotationsQuickFix.java new file mode 100644 index 000000000..58962f725 --- /dev/null +++ b/src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/codeAction/proposal/quickfix/ReplaceAnnotationsQuickFix.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * 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 implementation + *******************************************************************************/ +package io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.codeAction.proposal.quickfix; + +import com.google.gson.JsonArray; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiModifierListOwner; +import com.intellij.psi.util.PsiTreeUtil; +import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.JDTUtils; +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.lsp4mp4ij.psi.core.java.corrections.proposal.ReplaceAnnotationProposal; +import io.openliberty.tools.intellij.util.ExceptionUtil; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.Diagnostic; + +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.JDTUtils.getSimpleName; + +/** + * Abstract base class for quickfixes that replace annotations. + * Provides common functionality for extracting annotation data from diagnostics + * and creating code actions that replace multiple annotations with a single annotation. + */ +public abstract class ReplaceAnnotationsQuickFix extends InsertAnnotationMissingQuickFix { + + /** Logger object to record events for this class. */ + private static final Logger LOGGER = Logger.getLogger(ReplaceAnnotationsQuickFix.class.getName()); + + /** + * Constructor. + * + * @param annotation The fully qualified name of the annotation to insert + */ + public ReplaceAnnotationsQuickFix(String annotation) { + super(annotation); + } + + /** + * Extracts the list of annotation fully qualified names from diagnostic data. + * + * @param diagnostic The diagnostic containing annotation data + * @return List of fully qualified annotation names, or null if data is invalid + */ + private List extractAnnotationsFromDiagnostic(Diagnostic diagnostic) { + JsonArray diagnosticData = (JsonArray) diagnostic.getData(); + if (diagnosticData == null || diagnosticData.size() == 0) { + return null; + } + + return IntStream.range(0, diagnosticData.size()) + .mapToObj(idx -> diagnosticData.get(idx).getAsString()) + .collect(Collectors.toList()); + } + + /** + * {@inheritDoc} + */ + @Override + protected void insertAnnotations(Diagnostic diagnostic, JavaCodeActionContext context, + List codeActions) { + List annotationsToRemove = extractAnnotationsFromDiagnostic(diagnostic); + if (annotationsToRemove == null) { + return; + } + + // Get the code action label from subclass + String name = getCodeActionLabel(annotationsToRemove); + + // Create code action + codeActions.add(JDTUtils.createCodeAction(context, diagnostic, name, getParticipantId())); + } + + /** + * {@inheritDoc} + */ + @Override + public CodeAction resolveCodeAction(JavaCodeActionResolveContext context) { + final CodeAction toResolve = context.getUnresolved(); + String name = toResolve.getTitle(); + + // Get the diagnostic to extract annotations to remove + Diagnostic diagnostic = toResolve.getDiagnostics().get(0); + List annotationsToRemove = extractAnnotationsFromDiagnostic(diagnostic); + + if (annotationsToRemove == null) { + return toResolve; + } + + // Convert to array of fully qualified names for ReplaceAnnotationProposal + String[] fqNames = annotationsToRemove.toArray(new String[0]); + + PsiElement node = context.getCoveringNode(); + PsiModifierListOwner parentType = PsiTreeUtil.getParentOfType(node, PsiClass.class); + + // Create a proposal that replaces all annotations + ChangeCorrectionProposal proposal = new ReplaceAnnotationProposal(name, context.getCompilationUnit(), + context.getASTRoot(), parentType, 0, getAnnotations()[0], + context.getSource().getCompilationUnit(), fqNames); + + ExceptionUtil.executeWithWorkspaceEditHandling(context, proposal, toResolve, LOGGER, + "Unable to create workspace edit for code action to replace annotations"); + + return toResolve; + } + + /** + * Returns the code action label for the given list of annotation fully qualified names. + * Subclasses should override this to provide custom labels based on the annotations to remove. + * + * @param annotationsToRemove List of fully qualified annotation names to be removed + * @return The code action label + */ + protected abstract String getCodeActionLabel(List annotationsToRemove); +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 200ec3236..2a215b358 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -558,6 +558,10 @@ group="jakarta" targetDiagnostic="jakarta-cdi#InvalidDependentScopeWithConditionalObserver" implementationClass="io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.cdi.RemoveObserverAnnotationQuickFix"/> +