Skip to content

Commit 2db0e47

Browse files
committed
Merge branch 'lsp4jakarta-0.2.7-release-branch' of https://github.com/OpenLiberty/liberty-tools-intellij into cdi_wildcard_param_types_diagnostic
2 parents 7a205b4 + 7dd4d2e commit 2db0e47

40 files changed

Lines changed: 1604 additions & 106 deletions

File tree

src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/AbstractDiagnosticsCollector.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,22 +213,37 @@ protected static String getMatchedJavaElementName(PsiClass type, String javaElem
213213

214214
/**
215215
* Returns matched Java element fully qualified names.
216+
* This is the core implementation that accepts Collections for maximum flexibility.
216217
*
217218
* @param type the type representing the class
218-
* @param javaElementNames Java element names
219-
* @param javaElementFQNames given fully qualified name array
219+
* @param javaElementNames Java element names collection (Set or List)
220+
* @param javaElementFQNames given fully qualified name collection (Set or List)
220221
* @return matched Java element fully qualified names
221222
*/
222-
protected static List<String> getMatchedJavaElementNames(PsiClass type, String[] javaElementNames,
223-
String[] javaElementFQNames) {
224-
return Stream.of(javaElementFQNames).filter(fqName -> {
225-
boolean anyMatch = Stream.of(javaElementNames).anyMatch(name -> {
223+
protected static List<String> getMatchedJavaElementNames(PsiClass type, Collection<String> javaElementNames,
224+
Collection<String> javaElementFQNames) {
225+
return javaElementFQNames.stream().filter(fqName -> {
226+
boolean anyMatch = javaElementNames.stream().anyMatch(name -> {
226227
return isMatchedJavaElement(type, name, fqName);
227228
});
228229
return anyMatch;
229230
}).collect(Collectors.toList());
230231
}
231232

233+
/**
234+
* Returns matched Java element fully qualified names.
235+
* Convenience overload that accepts arrays and delegates to the Collection-based method.
236+
*
237+
* @param type the type representing the class
238+
* @param javaElementNames Java element names array
239+
* @param javaElementFQNames given fully qualified name array
240+
* @return matched Java element fully qualified names
241+
*/
242+
protected static List<String> getMatchedJavaElementNames(PsiClass type, String[] javaElementNames,
243+
String[] javaElementFQNames) {
244+
return getMatchedJavaElementNames(type, Arrays.asList(javaElementNames), Arrays.asList(javaElementFQNames));
245+
}
246+
232247
/**
233248
* Returns true if the given fully qualified name ends with the given name and
234249
* false otherwise

src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/DiagnosticsUtils.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.intellij.openapi.project.Project;
1717
import com.intellij.psi.*;
1818
import com.intellij.psi.search.GlobalSearchScope;
19+
import com.intellij.psi.tree.IElementType;
1920

2021
import java.beans.Introspector;
2122
import java.util.ArrayList;
@@ -160,4 +161,41 @@ public static List<PsiClass> collectSuperClasses(PsiClass psiClass) {
160161
}
161162
return superClasses;
162163
}
164+
165+
/**
166+
* Returns {@code true} if the given {@code @Priority} annotation carries a
167+
* negative integer value.
168+
*
169+
* <p>Handles two PSI representations of a negative integer literal:
170+
* <ul>
171+
* <li>A {@link PsiPrefixExpression} with a {@code MINUS} operator wrapping a
172+
* {@link PsiLiteralExpression} (e.g. {@code -1}).</li>
173+
* <li>A raw text value that can be parsed as a negative integer.</li>
174+
* </ul>
175+
*
176+
* @param priorityAnnotation the {@code @Priority} annotation to inspect;
177+
* must not be {@code null}
178+
* @return {@code true} if the priority value is negative; {@code false} otherwise
179+
*/
180+
public static boolean isNegativePriorityValue(PsiAnnotation priorityAnnotation) {
181+
PsiAnnotationMemberValue valueAttr = priorityAnnotation.findAttributeValue("value");
182+
if (valueAttr == null) {
183+
return false;
184+
}
185+
186+
// Case 1: literal negative integer written as -<number> (PsiPrefixExpression)
187+
if (valueAttr instanceof PsiPrefixExpression prefix) {
188+
IElementType op = prefix.getOperationSign().getTokenType();
189+
if (JavaTokenType.MINUS.equals(op) && prefix.getOperand() instanceof PsiLiteralExpression literal) {
190+
return literal.getValue() instanceof Integer;
191+
}
192+
}
193+
194+
// Case 2: fall back to text-based parsing (covers numeric constant expressions)
195+
try {
196+
return Integer.parseInt(valueAttr.getText()) < 0;
197+
} catch (NumberFormatException e) {
198+
return false;
199+
}
200+
}
163201
}

src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/annotations/AnnotationDiagnosticsCollector.java

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ private void validateResourceFields(PsiJavaFile unit, List<Diagnostic> diagnosti
249249
* validatePriority
250250
* This method validates priority values to check whether any negative values
251251
* have been applied.
252-
*
252+
*
253253
* @param unit
254254
* @param diagnostics
255255
* @param element
@@ -260,21 +260,13 @@ private void validatePriority(PsiJavaFile unit,
260260
PsiElement element,
261261
PsiAnnotation annotation) {
262262

263-
// Priority is valid only for elements that are either classes or method
264-
// parameters.
263+
// Priority is valid only for elements that are either classes or method parameters.
265264
if (element instanceof PsiClass || element instanceof PsiParameter) {
266-
PsiAnnotationMemberValue value = annotation.findAttributeValue("value");
267-
if (value instanceof PsiPrefixExpression prefix
268-
&& prefix.getOperand() instanceof PsiLiteralExpression literal &&
269-
literal.getValue() instanceof Integer) {
270-
if (JavaTokenType.MINUS.equals(prefix.getOperationSign().getTokenType())) {
271-
String diagnosticMessage = Messages.getMessage(
272-
"PriorityShouldBeNonNegative");
273-
diagnostics.add(createDiagnostic(annotation, unit, diagnosticMessage,
274-
AnnotationConstants.DIAGNOSTIC_CODE_PRIORITY_SHOULD_BE_NON_NEGATIVE, null,
275-
DiagnosticSeverity.Warning));
276-
}
277-
265+
if (DiagnosticsUtils.isNegativePriorityValue(annotation)) {
266+
String diagnosticMessage = Messages.getMessage("PriorityShouldBeNonNegative");
267+
diagnostics.add(createDiagnostic(annotation, unit, diagnosticMessage,
268+
AnnotationConstants.DIAGNOSTIC_CODE_PRIORITY_SHOULD_BE_NON_NEGATIVE, null,
269+
DiagnosticSeverity.Warning));
278270
}
279271
}
280272
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 IBM Corporation and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* IBM Corporation - initial API and implementation
12+
*******************************************************************************/
13+
14+
package io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.cdi;
15+
16+
import java.util.ArrayList;
17+
import java.util.List;
18+
import java.util.stream.Stream;
19+
20+
import com.intellij.psi.*;
21+
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.AbstractDiagnosticsCollector;
22+
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.Messages;
23+
import org.eclipse.lsp4j.Diagnostic;
24+
import org.eclipse.lsp4j.DiagnosticSeverity;
25+
26+
/**
27+
* CDI diagnostics collector that validates decorator delegate injection points.
28+
*/
29+
public class CdiDecoratorDiagnosticsCollector extends AbstractDiagnosticsCollector {
30+
31+
@Override
32+
protected String getDiagnosticSource() {
33+
return ManagedBeanConstants.DIAGNOSTIC_SOURCE;
34+
}
35+
36+
@Override
37+
public void collectDiagnostics(PsiJavaFile unit, List<Diagnostic> diagnostics) {
38+
if (unit == null) {
39+
return;
40+
}
41+
42+
for (PsiClass type : unit.getClasses()) {
43+
validateDecorator(type, unit, diagnostics);
44+
}
45+
}
46+
47+
/**
48+
* Validates that a decorator class declares exactly one @Delegate injection point.
49+
*
50+
* @param type
51+
* @param unit
52+
* @param diagnostics
53+
*/
54+
private void validateDecorator(PsiClass type, PsiJavaFile unit, List<Diagnostic> diagnostics) {
55+
String[] typeAnnotations = Stream.of(type.getAnnotations())
56+
.map(PsiAnnotation::getQualifiedName)
57+
.toArray(String[]::new);
58+
if (getMatchedJavaElementNames(type, typeAnnotations, new String[] { ManagedBeanConstants.DECORATOR_FQ_NAME }).isEmpty()) {
59+
return;
60+
}
61+
List<PsiElement> delegateElements = new ArrayList<>();
62+
collectDelegates(type.getFields(), type, delegateElements);
63+
for (PsiMethod method : type.getMethods()) {
64+
collectDelegates(method.getParameterList().getParameters(), type, delegateElements);
65+
}
66+
reportInvalidDelegateCountDiagnostics(type, unit, diagnostics, delegateElements);
67+
}
68+
69+
/**
70+
* collectDelegates
71+
* Helper method to collect delegates from any Java elements
72+
*
73+
* @param elements
74+
* @param type
75+
* @param delegateElements
76+
*/
77+
private void collectDelegates(PsiModifierListOwner[] elements, PsiClass type, List<PsiElement> delegateElements) {
78+
for (PsiModifierListOwner element : elements) {
79+
String[] annotations = Stream.of(element.getAnnotations())
80+
.map(PsiAnnotation::getQualifiedName)
81+
.toArray(String[]::new);
82+
83+
if (!getMatchedJavaElementNames(type, annotations,
84+
new String[] { ManagedBeanConstants.DELEGATE_FQ_NAME }).isEmpty()) {
85+
delegateElements.add(element);
86+
}
87+
}
88+
}
89+
/**
90+
* reportInvalidDelegateCountDiagnostics
91+
* Reports diagnostics when a decorator has an invalid number of @Delegate injection points.
92+
*
93+
* @param type
94+
* @param unit
95+
* @param diagnostics
96+
* @param delegateElements
97+
*/
98+
private void reportInvalidDelegateCountDiagnostics(PsiClass type, PsiJavaFile unit, List<Diagnostic> diagnostics, List<PsiElement> delegateElements) {
99+
int delegateCount = delegateElements.size();
100+
if (delegateCount == 0) {
101+
diagnostics.add(createDiagnostic(type, unit,
102+
Messages.getMessage("MissingDelegateInDecorator"),
103+
ManagedBeanConstants.DIAGNOSTIC_CODE_INVALID_DECORATOR_DELEGATE, null,
104+
DiagnosticSeverity.Error));
105+
} else if(delegateCount > 1) {
106+
String message = Messages.getMessage("DecoratorWithMultipleDelegates", delegateCount);
107+
for (PsiElement delegateElement : delegateElements) {
108+
diagnostics.add(createDiagnostic(delegateElement, unit, message,
109+
ManagedBeanConstants.DIAGNOSTIC_CODE_INVALID_DECORATOR_DELEGATE, null,
110+
DiagnosticSeverity.Error));
111+
}
112+
}
113+
}
114+
}
115+

src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/cdi/ManagedBeanConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public class ManagedBeanConstants {
3232
public static final String STATELESS_FQ_NAME = "jakarta.ejb.Stateless";
3333
public static final String NORMAL_SCOPE_FQ_NAME = "jakarta.enterprise.context.NormalScope";
3434
public static final String NAMED_FQ_NAME = "jakarta.inject.Named";
35+
public static final String DELEGATE_FQ_NAME = "jakarta.decorator.Delegate";
3536

3637
public static final String DIAGNOSTIC_SOURCE = "jakarta-cdi";
3738
public static final String DIAGNOSTIC_CODE = "InvalidManagedBeanAnnotation";
@@ -49,6 +50,7 @@ public class ManagedBeanConstants {
4950
public static final String DIAGNOSTIC_INJECT_MULTIPLE_METHOD_PARAM = "InvalidInjectAnnotationOnMultipleMethodParams";
5051
public static final String DIAGNOSTIC_OBSERVES_OBSERVESASYNC_PARAM_CONFLICT = "InvalidObservesObservesAsyncMethodParams";
5152
public static final String DIAGNOSTIC_CODE_INTERCEPTOR_DECORATOR_OBSERVER = "InvalidInterceptorOrDecoratorWithObserverMethod";
53+
public static final String DIAGNOSTIC_CODE_INVALID_DECORATOR_DELEGATE = "InvalidDecoratorDelegateInjectionPoints";
5254
public static final String DIAGNOSTIC_CODE_DEPENDENT_CONDITIONAL_OBSERVER = "InvalidDependentScopeWithConditionalObserver";
5355
public static final String DIAGNOSTIC_MULTIPLE_OBSERVER_PARAMS = "InvalidMultipleObserverParams";
5456
public static final String DIAGNOSTIC_CODE_INTERCEPTOR_DECORATOR_ILLEGAL_SCOPE = "InvalidInterceptorOrDecorator";

src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/interceptor/Constants.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class Constants {
3030
public static final String DIAGNOSTIC_CODE_INTERCEPTOR_FINAL = "InvalidInterceptorMethodAnnotationOnFinalMethod";
3131
public static final String DIAGNOSTIC_CODE_INTERCEPTOR_ABSTRACT = "InvalidInterceptorMethodAnnotationOnAbstractMethod";
3232
public static final String DIAGNOSTIC_CODE_INTERCEPTOR_STATIC = "InvalidInterceptorMethodAnnotationOnStaticMethod";
33+
public static final String DIAGNOSTIC_CODE_INTERCEPTOR_NEGATIVE_PRIORITY = "InterceptorNegativePriority";
3334

3435
private static final String AROUND_CONSTRUCT_FQ_NAME = "jakarta.interceptor.AroundConstruct";
3536

@@ -54,4 +55,6 @@ public class Constants {
5455

5556
public static final Set<String> LIFECYCLE_CALLBACK_INTERCEPTOR_METHODS = Set.of(AROUND_CONSTRUCT_FQ_NAME, PRE_DESTROY_FQ_NAME, POST_CONSTRUCT_FQ_NAME);
5657

58+
public static final String PRIORITY_FQ_NAME = "jakarta.annotation.Priority";
59+
5760
}

src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/interceptor/InterceptorDiagnosticsParticipant.java

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.gson.JsonArray;
2121
import com.intellij.psi.*;
2222
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.AbstractDiagnosticsCollector;
23+
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.DiagnosticsUtils;
2324
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.PositionUtils;
2425
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.ASTUtils;
2526
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.Messages;
@@ -57,11 +58,13 @@ public void collectDiagnostics(PsiJavaFile unit, List<Diagnostic> diagnostics) {
5758
//Build the diagnostics if the parent class is Interceptor type and is abstract.
5859
// Also, checks for missing public no-args constructor.
5960
validateAbstractClassAndNoArgsConstructor(unit, diagnostics, type);
60-
for (PsiClass innerClass : type.getInnerClasses()) {
61-
//Build the diagnostics if the child class is Interceptor type and is abstract.
62-
// Also, checks for missing public no-args constructor.
63-
validateAbstractClassAndNoArgsConstructor(unit, diagnostics, innerClass);
64-
}
61+
checkNegativePriority(unit, diagnostics, type);
62+
for (PsiClass innerClass : type.getInnerClasses()) {
63+
//Build the diagnostics if the child class is Interceptor type and is abstract.
64+
// Also, checks for missing public no-args constructor.
65+
validateAbstractClassAndNoArgsConstructor(unit, diagnostics, innerClass);
66+
checkNegativePriority(unit, diagnostics, innerClass);
67+
}
6568
PsiMethod[] allMethods = type.getMethods();
6669
for (PsiMethod method : allMethods) {
6770
List<String> interceptorTypeMethodAnnotations = containsAnyMatchingAnnotations(type, method, Constants.INTERCEPTOR_METHODS);
@@ -91,7 +94,7 @@ public void collectDiagnostics(PsiJavaFile unit, List<Diagnostic> diagnostics) {
9194
}
9295
}
9396
}
94-
}
97+
}
9598
Collection<PsiMethod> allMethodDeclarations = ASTUtils.getAllMethodDeclarations(unit);
9699
List<PsiMethod> methodsMissingProceedInvocation = allMethodDeclarations.stream().filter(m -> missingInterceptorMethodProceedInvocation(m, unit)).collect(Collectors.toList());
97100
for(PsiMethod invokeMethod: methodsMissingProceedInvocation){
@@ -187,4 +190,39 @@ private String getSimpleAnnotationNames(List<String> annotations) {
187190
List<String> simpleAnnotationNames = annotations.stream().map(name -> JDTUtils.getSimpleName(name)).distinct().collect(Collectors.toList());
188191
return String.join(", ", simpleAnnotationNames);
189192
}
193+
194+
/**
195+
* Checks if an @Interceptor class has a @Priority annotation with a negative value.
196+
* According to the Jakarta Interceptors specification, negative priority values are
197+
* reserved for future use and should not be used.
198+
*
199+
* @param unit the Java file containing the class
200+
* @param diagnostics the list to add diagnostics to
201+
* @param type the class to check
202+
*/
203+
private void checkNegativePriority(PsiJavaFile unit, List<Diagnostic> diagnostics, PsiClass type) {
204+
if (!isInterceptorType(type)) {
205+
return;
206+
}
207+
208+
// Check if the class has @Priority annotation
209+
PsiAnnotation priorityAnnotation = null;
210+
for (PsiAnnotation annotation : type.getAnnotations()) {
211+
if (isMatchedJavaElement(type, annotation.getQualifiedName(), PRIORITY_FQ_NAME)) {
212+
priorityAnnotation = annotation;
213+
break;
214+
}
215+
}
216+
217+
if (priorityAnnotation == null) {
218+
return;
219+
}
220+
221+
if (DiagnosticsUtils.isNegativePriorityValue(priorityAnnotation)) {
222+
Range range = PositionUtils.toNameRange(priorityAnnotation);
223+
Diagnostic diagnostic = new Diagnostic(range, Messages.getMessage("InterceptorNegativePriority"));
224+
completeDiagnostic(diagnostic, DIAGNOSTIC_CODE_INTERCEPTOR_NEGATIVE_PRIORITY, DiagnosticSeverity.Error);
225+
diagnostics.add(diagnostic);
226+
}
227+
}
190228
}

src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/jsonb/JsonbConstants.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public class JsonbConstants {
2828
public static final String DIAGNOSTIC_CODE_NO_ARGS_CONSTRUCTOR_MISSING = "InvalidJsonBNoArgsConstructorMissing";
2929
public static final String DIAGNOSTIC_CODE_NON_STATIC_INNER_CLASS = "InvalidJsonBNonStaticInnerClass";
3030
public static final String DIAGNOSTIC_CODE_NON_PUBLIC_PROTECTED_STATIC_NESTED_CLASS = "InvalidJsonBNonPublicProtectedStaticNestedClass";
31+
public static final String DIAGNOSTIC_CODE_FROM_JSON_NULL_PARAMETER = "InvalidJsonbFromJsonNullParameter";
3132

3233
/* Annotation Constants */
3334
public static final String JSONB_PACKAGE = "jakarta.json.bind.annotation.";
@@ -56,4 +57,7 @@ public class JsonbConstants {
5657
JSONB_DATE_FORMAT, JSONB_NILLABLE, JSONB_NUMBER_FORMAT, JSONB_PROPERTY, JSONB_PROPERTY_ORDER,
5758
JSONB_TYPE_ADAPTER, JSONB_TYPE_DESERIALIZER, JSONB_TYPE_SERIALIZER, JSONB_VISIBILITY);
5859

60+
/* Jsonb fromJson constants */
61+
public static final String JSONB_FROM_JSON_PACKAGE = "jakarta.json.bind.Jsonb";
62+
public static final String FROM_JSON_METHOD = "fromJson";
5963
}

0 commit comments

Comments
 (0)