Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 };
Expand All @@ -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<String> INVALID_OBSERVES_OBSERVES_ASYNC_CONFLICTED_PARAMS = Set.of(OBSERVES_FQ_NAME, OBSERVES_ASYNC_FQ_NAME);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> 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=========
Expand Down Expand Up @@ -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<String> validateInterceptorDecoratorScopes(PsiClass type, PsiAnnotation[] typeAnnotations) {
List<String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> annotationFqNames) {
return annotationFqNames.stream()
.map(fqName -> "@" + getSimpleName(fqName))
.collect(Collectors.joining(", "));
}
}

Original file line number Diff line number Diff line change
@@ -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<String> 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<CodeAction> codeActions) {
List<String> 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<String> 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<String> annotationsToRemove);
}
4 changes: 4 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,10 @@
group="jakarta"
targetDiagnostic="jakarta-cdi#InvalidDependentScopeWithConditionalObserver"
implementationClass="io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.cdi.RemoveObserverAnnotationQuickFix"/>
<javaCodeActionParticipant kind="quickfix"
group="jakarta"
targetDiagnostic="jakarta-cdi#InvalidInterceptorOrDecorator"
implementationClass="io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.cdi.ReplaceInvalidScopesWithDependentQuickFix"/>
<javaCodeActionParticipant kind="quickfix"
group="jakarta"
targetDiagnostic="jakarta-cdi#InvalidDependentScopeWithConditionalObserver"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ ManagedBeanDependentScopeConditionalObserver = Beans with scope @Dependent may n
SingletonSessionBeanInvalidScope = A singleton session bean must be annotated with either @ApplicationScoped or @Dependent.
ManagedBeanMultipleObserverParams = Parameters {0} are annotated with @Observes or @ObservesAsync, but a method cannot contain more than one such parameter.
StatelessSessionBeanWithIllegalScope = A stateless session bean belongs to the @Dependent scope. Any other scope is invalid.
InterceptorOrDecoratorWithIllegalScope = Interceptors and decorators must be annotated with the @Dependent scope. Any other scope is invalid.

# ReplaceInvalidScopesWithDependentQuickFix
ReplaceInvalidScopesWithDependent = Replace {0} with @Dependent

# ManagedBeanNoArgConstructorQuickFix
AddProtectedConstructor = Add a no-arg protected constructor to this class
Expand Down
Loading
Loading