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
@@ -0,0 +1,59 @@
package io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.codeAction.common;

import com.intellij.psi.*;
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.jsonb.JsonbConstants;
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.jsonp.JsonpConstants;
import org.apache.commons.lang3.StringUtils;

/**
* Utility class for common PSI method call operations.
*/
public class PsiMethodCallUtils {

/**
* Checks if an expression is a null literal or a cast expression containing a null literal.
* This is useful for detecting null arguments passed to methods that don't accept null parameters.
*
* @param arg the expression to check
* @return true if the expression is null or a cast of null, false otherwise
*/
public static boolean isInvalidNullArgument(PsiExpression arg) {
return (arg instanceof PsiLiteralExpression lit && lit.getValue() == null)
|| (arg instanceof PsiTypeCastExpression cast
&& cast.getOperand() instanceof PsiLiteralExpression
&& ((PsiLiteralExpression) cast.getOperand()).getValue() == null);
}

/**
* isMatchedMethodFQName
* Method is used to identify passed method invocations
*
* @param mce
* @param methodParentTypeFQ
* @return boolean
*/
public static boolean isMatchedMethodFQName(PsiMethodCallExpression mce, String methodParentTypeFQ) {
PsiMethod method = mce.resolveMethod();
if(getMethodName(method).equals(JsonpConstants.CREATE_POINTER)){
return mce.getArgumentList().getExpressionCount() == JsonpConstants.EXPRESSION_COUNT_CREATE_POINTER
&& methodParentTypeFQ.equals(method.getContainingClass().getQualifiedName());
} else if(getMethodName(method).equals(JsonpConstants.JAKARTA_JSON_BUILDER_ADD_METHOD) ||
getMethodName(method).equals(JsonbConstants.FROM_JSON_METHOD)){
return methodParentTypeFQ.equals(method.getContainingClass().getQualifiedName());
}
return false;
}

/**
* Check if valid method exists
*
* @param method
* @return
*/
public static String getMethodName(PsiMethod method) {
if(method != null && method.getClass() != null){
return method.getName();
}
return StringUtils.EMPTY;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class JsonbConstants {
public static final String DIAGNOSTIC_CODE_NO_ARGS_CONSTRUCTOR_MISSING = "InvalidJsonBNoArgsConstructorMissing";
public static final String DIAGNOSTIC_CODE_NON_STATIC_INNER_CLASS = "InvalidJsonBNonStaticInnerClass";
public static final String DIAGNOSTIC_CODE_NON_PUBLIC_PROTECTED_STATIC_NESTED_CLASS = "InvalidJsonBNonPublicProtectedStaticNestedClass";
public static final String DIAGNOSTIC_CODE_FROM_JSON_NULL_PARAMETER = "InvalidJsonbFromJsonNullParameter";

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

/* Jsonb fromJson constants */
public static final String JSONB_FROM_JSON_PACKAGE = "jakarta.json.bind.Jsonb";
public static final String FROM_JSON_METHOD = "fromJson";
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@

import com.intellij.psi.*;
import com.intellij.psi.impl.PsiClassImplUtil;
import com.intellij.psi.util.PsiTreeUtil;
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.AbstractDiagnosticsCollector;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.JDTUtils;
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.JsonPropertyUtils;
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.Messages;
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.PositionUtils;
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.codeAction.common.PsiMethodCallUtils;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.eclipse.lsp4j.Range;

/**
* This class contains logic for Jsonb diagnostics:
Expand Down Expand Up @@ -132,8 +136,11 @@ public void collectDiagnostics(PsiJavaFile unit, List<Diagnostic> diagnostics) {
// Jsonb deseriazation diagnostics
generateJsonbDeserializerDiagnostics(unit, diagnostics, jsonbtypeParent, false,
missingParentNoArgsConstructor, false, type);
}
}
}

// Collect diagnostics for Jsonb.fromJson() method invocations with null parameters
collectJsonbFromJsonNullParameterDiagnostics(unit, diagnostics);
}

/**
* @param element
Expand Down Expand Up @@ -351,4 +358,39 @@ private boolean hasJsonbAnnotationOtherThanTransient(List<String> jsonbAnnotatio
return true;
return false;
}

/**
* Collects diagnostics for Jsonb.fromJson() method invocations where null is passed as a parameter.
* According to the Jakarta JSON Binding specification, the fromJson() method must not accept null parameters.
*
* @param unit the compilation unit
* @param diagnostics the list to add diagnostics to
*/
private void collectJsonbFromJsonNullParameterDiagnostics(PsiJavaFile unit, List<Diagnostic> diagnostics) {
if (unit == null) {
return;
}
// Find all method call expressions in the file
Collection<PsiMethodCallExpression> allMethodInvocations = PsiTreeUtil.findChildrenOfType(unit, PsiMethodCallExpression.class);
List<PsiMethodCallExpression> fromJsonInvocations = new ArrayList<>();
// Filter for fromJson() method calls
for (PsiMethodCallExpression mi : allMethodInvocations) {
if (PsiMethodCallUtils.isMatchedMethodFQName(mi, JsonbConstants.JSONB_FROM_JSON_PACKAGE)) {
fromJsonInvocations.add(mi);
}
}
// Create diagnostics for fromJson() calls with null parameters
for (PsiMethodCallExpression methodCall : fromJsonInvocations) {
PsiExpression[] args = methodCall.getArgumentList().getExpressions();
for (PsiExpression arg : args) {
if (PsiMethodCallUtils.isInvalidNullArgument(arg)) {
String msg = Messages.getMessage("ErrorMessageJsonbFromJsonNullParameter");
Range range = PositionUtils.toNameRange(arg);
Diagnostic diagnostic = new Diagnostic(range, msg);
completeDiagnostic(diagnostic, JsonbConstants.DIAGNOSTIC_CODE_FROM_JSON_NULL_PARAMETER);
diagnostics.add(diagnostic);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
import java.util.List;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import org.apache.commons.lang3.StringUtils;
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.AbstractDiagnosticsCollector;
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.Messages;
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.PositionUtils;
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.codeAction.common.PsiMethodCallUtils;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Range;

Expand Down Expand Up @@ -49,13 +49,13 @@ public void collectDiagnostics(PsiJavaFile unit, List<Diagnostic> diagnostics) {
List<PsiMethodCallExpression> createArrayBuilderMethodInvocations = new ArrayList<>();

for (PsiMethodCallExpression mi : allMethodInvocations) {
if (isMatchedMethodFQName(mi, JsonpConstants.JSON_FQ_NAME)) {
if (PsiMethodCallUtils.isMatchedMethodFQName(mi, JsonpConstants.JSON_FQ_NAME)) {
createPointerInvocations.add(mi);
}
if (isMatchedMethodFQName(mi, JsonpConstants.JAKARTA_JSON_OBJECT_BUILDER_FQ_NAME)){
if (PsiMethodCallUtils.isMatchedMethodFQName(mi, JsonpConstants.JAKARTA_JSON_OBJECT_BUILDER_FQ_NAME)){
createObjectBuilderMethodInvocations.add(mi);
}
if (isMatchedMethodFQName(mi, JsonpConstants.JAKARTA_JSON_ARRAY_BUILDER_FQ_NAME)){
if (PsiMethodCallUtils.isMatchedMethodFQName(mi, JsonpConstants.JAKARTA_JSON_ARRAY_BUILDER_FQ_NAME)){
createArrayBuilderMethodInvocations.add(mi);
}
}
Expand Down Expand Up @@ -94,15 +94,15 @@ private void createDiagnosticsForMethodInvocations(List<Diagnostic> diagnostics,
List<PsiMethodCallExpression> builderMethodInvocations,
String msg, String errCode) {
for(PsiMethodCallExpression m: builderMethodInvocations){
if(getMethodName(m.resolveMethod()).equals(JsonpConstants.CREATE_POINTER)){
if(PsiMethodCallUtils.getMethodName(m.resolveMethod()).equals(JsonpConstants.CREATE_POINTER)){
PsiExpression arg = m.getArgumentList().getExpressions()[0];
if(isInvalidArgumentCreatePointer(arg)) {
buildInvalidArgumentDiagnostic(diagnostics, msg, errCode, arg);
}
} else if(getMethodName(m.resolveMethod()).equals(JsonpConstants.JAKARTA_JSON_BUILDER_ADD_METHOD)){
} else if(PsiMethodCallUtils.getMethodName(m.resolveMethod()).equals(JsonpConstants.JAKARTA_JSON_BUILDER_ADD_METHOD)){
PsiExpression[] args = m.getArgumentList().getExpressions();
for(PsiExpression arg : args) {
if(isInvalidNullArgument(arg)) {
if(PsiMethodCallUtils.isInvalidNullArgument(arg)) {
buildInvalidArgumentDiagnostic(diagnostics, msg, errCode, arg);
}
}
Expand All @@ -125,51 +125,6 @@ private void buildInvalidArgumentDiagnostic(List<Diagnostic> diagnostics, String
diagnostics.add(diagnostic);
}

/**
* Method is used to check if value of arg passed or Cast Expression inside passed arg is null
*
* @param arg
* @return
*/
private boolean isInvalidNullArgument(PsiExpression arg) {
return (arg instanceof PsiLiteralExpression lit && lit.getValue() == null)
|| (arg instanceof PsiTypeCastExpression cast
&& cast.getOperand() instanceof PsiLiteralExpression
&& ((PsiLiteralExpression) cast.getOperand()).getValue() == null);
}

/**
* isMatchedMethodFQName
* Method is used to identify passed method invocations
*
* @param mce
* @param methodParentTypeFQ
* @return boolean
*/
private boolean isMatchedMethodFQName(PsiMethodCallExpression mce, String methodParentTypeFQ) {
PsiMethod method = mce.resolveMethod();
if(getMethodName(method).equals(JsonpConstants.CREATE_POINTER)){
return mce.getArgumentList().getExpressionCount() == JsonpConstants.EXPRESSION_COUNT_CREATE_POINTER
&& methodParentTypeFQ.equals(method.getContainingClass().getQualifiedName());
} else if(getMethodName(method).equals(JsonpConstants.JAKARTA_JSON_BUILDER_ADD_METHOD)){
return methodParentTypeFQ.equals(method.getContainingClass().getQualifiedName());
}
return false;
}

/**
* Check if valid method exists
*
* @param method
* @return
*/
private String getMethodName(PsiMethod method) {
if(method != null && method.getClass() != null){
return method.getName();
}
return StringUtils.EMPTY;
}

private boolean isInvalidArgumentCreatePointer(PsiExpression arg) {
if (arg instanceof PsiLiteralExpression) {
if (((PsiLiteralExpression) arg).getValue() instanceof String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ ErrorMessageJsonbPropertyUniquenessField = Multiple fields or properties with @J
ErrorMessageJsonbNoArgConstructorMissing = Missing Public or Protected NoArgsConstructor: Class {0} uses JSON Binding annotations, but does not declare a public or protected no-argument constructor.
ErrorMessageJsonbInnerNonStatic = Cannot deserialize class {0} because it is not static. Please declare the class as static for JSONB deserialization.
ErrorMessageJsonbNonPublicProtectedStaticNestedClass = Static nested class {0} must be public or protected for JSON Binding deserialization. Private and packaged private static nested classes are not supported.
ErrorMessageJsonbFromJsonNullParameter = The parameter of the fromJson() method must not be null.

# JsonpDiagnosticCollector
CreatePointerErrorMessage = Json.createPointer target must be a sequence of '/' prefixed tokens or an empty String.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2647,4 +2647,29 @@ public void JsonbDeserialization() throws Exception {
CodeAction insertNoArgPubConstructorChild = JakartaForJavaAssert.ca(uri, Messages.getMessage("AddNoArgPublicConstructor"), missingPubOrProConstructorChild, addPublicConstructorChildEdit);
JakartaForJavaAssert.assertJavaCodeAction(codeActionParams2, utils, insertNoArgProConstructorChild, insertNoArgPubConstructorChild);
}

@Test
public void testJsonbFromJsonNullParameters() 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/jsonb/JsonbFromJsonNullParameter.java");
String uri = VfsUtilCore.virtualToIoFile(javaFile).toURI().toString();

JakartaJavaDiagnosticsParams diagnosticsParams = new JakartaJavaDiagnosticsParams();
diagnosticsParams.setUris(Arrays.asList(uri));

// Test null first parameter with cast: jsonb.fromJson((String) null, Person.class)
Diagnostic nullFirstParamWithCast = JakartaForJavaAssert.d(47, 38, 51,
"The parameter of the fromJson() method must not be null.",
DiagnosticSeverity.Error, "jakarta-jsonb", "InvalidJsonbFromJsonNullParameter");

// Test null second parameter: jsonb.fromJson(json, null)
Diagnostic nullSecondParam = JakartaForJavaAssert.d(57, 46, 50,
"The parameter of the fromJson() method must not be null.",
DiagnosticSeverity.Error, "jakarta-jsonb", "InvalidJsonbFromJsonNullParameter");

JakartaForJavaAssert.assertJavaDiagnostics(diagnosticsParams, utils, nullFirstParamWithCast, nullSecondParam);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*******************************************************************************
* 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.sample.jakarta.jsonb;

import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;

/**
* Test class for Jsonb.fromJson() null parameter diagnostics.
* According to Jakarta JSON Binding specification, fromJson() must not accept null parameters.
*/
public class JsonbFromJsonNullParameter {

static class Person {
private int id;
private String name;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

public void testNullFirstParameter() {
try (Jsonb jsonb = JsonbBuilder.create()) {
// ERROR: First parameter (JSON input) is null
Person p = jsonb.fromJson((String) null, Person.class);
} catch (Exception e) {
e.printStackTrace();
}
}

public void testNullSecondParameter() {
try (Jsonb jsonb = JsonbBuilder.create()) {
String json = "{ \"id\": 1, \"name\": \"Test\" }";
// ERROR: Second parameter (target type) is null
Object obj = jsonb.fromJson(json, null);
} catch (Exception ignored) {
}
}

public void testBothParametersNull() {
try (Jsonb jsonb = JsonbBuilder.create()) {
// ERROR: Both parameters are null
Object obj = jsonb.fromJson(null, null);
} catch (Exception e) {
e.printStackTrace();
}
}

public void testNullWithoutCast() {
try (Jsonb jsonb = JsonbBuilder.create()) {
// ERROR: First parameter is null without cast
Person p = jsonb.fromJson(null, Person.class);
} catch (Exception e) {
e.printStackTrace();
}
}

public void testValidUsage() {
try (Jsonb jsonb = JsonbBuilder.create()) {
String json = "{ \"id\": 1, \"name\": \"Valid\" }";
// VALID: Both parameters are non-null
Person p = jsonb.fromJson(json, Person.class);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Loading