Skip to content

Commit 98fb0b9

Browse files
Issue 1401: New diagnostics for PersistenceMapKeyDiagnostics (#1405)
* Introducing hasField to check the existence of PsiField * Accessor warning diagnostics for method and field * Add diagnostics for type check * Update copyright * Update PersistenceMapKeyDiagnosticsCollector.java Map FQDN constant * Testcases for following diagnostics: Access specifier diagnostic Method or Field type diagnostic * Testcase input files * Source code formatted * Change variable name to follow Oracle's guidelines * Refactor hasField method * Removed lines * Using StringUtils utility * Change from isEmpty to isNotBlank * Copyright update
1 parent b6bbe6d commit 98fb0b9

File tree

6 files changed

+238
-5
lines changed

6 files changed

+238
-5
lines changed

src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/persistence/PersistenceConstants.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2020, 2022 IBM Corporation, Ankush Sharma and others.
2+
* Copyright (c) 2020, 2025 IBM Corporation, Ankush Sharma and others.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -36,6 +36,11 @@ public class PersistenceConstants {
3636
/* MapKey Codes */
3737
public static final String DIAGNOSTIC_CODE_INVALID_ANNOTATION = "RemoveMapKeyorMapKeyClass";
3838
public static final String DIAGNOSTIC_CODE_MISSING_ATTRIBUTES = "SupplyAttributesToAnnotations";
39+
public static final String DIAGNOSTIC_CODE_INVALID_ACCESS_SPECIFIER = "InvalidMethodAccessSpecifier";
40+
public static final String DIAGNOSTIC_CODE_INVALID_METHOD_NAME = "InvalidMethodName";
41+
public static final String DIAGNOSTIC_CODE_FIELD_NOT_EXIST = "InvalidMapKeyAnnotationsFieldNotFound";
42+
public static final String DIAGNOSTIC_CODE_INVALID_RETURN_TYPE = "InvalidReturnTypeOfMethod";
43+
public static final String DIAGNOSTIC_CODE_INVALID_TYPE = "InvalidTypeOfField";
3944

4045
public final static String[] SET_OF_PERSISTENCE_ANNOTATIONS = {MAPKEY, MAPKEYCLASS, MAPKEYJOINCOLUMN};
4146
}

src/main/java/io/openliberty/tools/intellij/lsp4jakarta/lsp4ij/persistence/PersistenceMapKeyDiagnosticsCollector.java

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2020, 2024 IBM Corporation, Ankush Sharma and others.
2+
* Copyright (c) 2020, 2025 IBM Corporation, Ankush Sharma and others.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -14,11 +14,14 @@
1414
package io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.persistence;
1515

1616
import com.intellij.psi.*;
17+
import com.intellij.psi.util.InheritanceUtil;
1718
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.AbstractDiagnosticsCollector;
1819
import io.openliberty.tools.intellij.lsp4jakarta.lsp4ij.Messages;
20+
import org.apache.commons.lang3.StringUtils;
1921
import org.eclipse.lsp4j.Diagnostic;
2022
import org.eclipse.lsp4j.DiagnosticSeverity;
2123

24+
import java.beans.Introspector;
2225
import java.util.ArrayList;
2326
import java.util.Arrays;
2427
import java.util.List;
@@ -61,6 +64,7 @@ private void collectDiagnostics(PsiJavaFile unit, List<Diagnostic> diagnostics,
6164
List<PsiAnnotation> mapKeyJoinCols = new ArrayList<PsiAnnotation>();
6265
boolean hasMapKeyAnnotation = false;
6366
boolean hasMapKeyClassAnnotation = false;
67+
boolean hasTypeDiagnostics = false;
6468
PsiAnnotation[] allAnnotations = fieldOrProperty.getAnnotations();
6569
for (PsiAnnotation annotation : allAnnotations) {
6670
String matchedAnnotation = getMatchedJavaElementName(type, annotation.getQualifiedName(),
@@ -75,7 +79,15 @@ else if (PersistenceConstants.MAPKEYJOINCOLUMN.equals(matchedAnnotation)) {
7579
}
7680
}
7781
}
78-
if (hasMapKeyAnnotation && hasMapKeyClassAnnotation) {
82+
if (hasMapKeyAnnotation) {
83+
hasTypeDiagnostics = collectTypeDiagnostics(fieldOrProperty, "@MapKey", unit, diagnostics);
84+
collectAccessorDiagnostics(fieldOrProperty, type, unit, diagnostics);
85+
}
86+
if (hasMapKeyClassAnnotation) {
87+
hasTypeDiagnostics = collectTypeDiagnostics(fieldOrProperty, "@MapKeyClass", unit, diagnostics);
88+
collectAccessorDiagnostics(fieldOrProperty, type, unit, diagnostics);
89+
}
90+
if (!hasTypeDiagnostics && (hasMapKeyAnnotation && hasMapKeyClassAnnotation)) {
7991
//A single field or property cannot be annotated with both @MapKey and @MapKeyClass
8092
//Specification References:
8193
//https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/mapkey
@@ -92,6 +104,36 @@ else if (PersistenceConstants.MAPKEYJOINCOLUMN.equals(matchedAnnotation)) {
92104
}
93105
}
94106

107+
private boolean collectTypeDiagnostics(PsiJvmModifiersOwner fieldOrProperty, String attribute, PsiJavaFile unit,
108+
List<Diagnostic> diagnostics) {
109+
final String MAP_INTERFACE_FQDN = "java.util.Map";
110+
boolean hasTypeDiagnostics = false;
111+
PsiType fieldOrPropertyType = null;
112+
boolean isMapOrSubtype = false;
113+
String messageKey = null;
114+
String code = null;
115+
116+
if (fieldOrProperty instanceof PsiMethod method) {
117+
fieldOrPropertyType = method.getReturnType();
118+
messageKey = "MapKeyAnnotationsReturnTypeOfMethod";
119+
code = PersistenceConstants.DIAGNOSTIC_CODE_INVALID_RETURN_TYPE;
120+
} else if (fieldOrProperty instanceof PsiField field) {
121+
fieldOrPropertyType = field.getType();
122+
messageKey = "MapKeyAnnotationsTypeOfField";
123+
code = PersistenceConstants.DIAGNOSTIC_CODE_INVALID_TYPE;
124+
}
125+
if (fieldOrPropertyType instanceof PsiClassType classType) {
126+
PsiClass psiClass = classType.resolve();
127+
isMapOrSubtype = InheritanceUtil.isInheritor(psiClass, MAP_INTERFACE_FQDN);
128+
}
129+
if (!isMapOrSubtype) {
130+
hasTypeDiagnostics = true;
131+
diagnostics.add(createDiagnostic(fieldOrProperty, unit, Messages.getMessage(messageKey, attribute),
132+
code, null, DiagnosticSeverity.Error));
133+
}
134+
return hasTypeDiagnostics;
135+
}
136+
95137
private void validateMapKeyJoinColumnAnnotations(List<PsiAnnotation> annotations, PsiElement element,
96138
PsiJavaFile unit, List<Diagnostic> diagnostics) {
97139
String message = (element instanceof PsiMethod) ?
@@ -110,4 +152,42 @@ private void validateMapKeyJoinColumnAnnotations(List<PsiAnnotation> annotations
110152
}
111153
});
112154
}
155+
156+
private void collectAccessorDiagnostics(PsiJvmModifiersOwner fieldOrProperty, PsiClass type, PsiJavaFile unit,
157+
List<Diagnostic> diagnostics) {
158+
String messageKey = null;
159+
String code = null;
160+
if (fieldOrProperty instanceof PsiMethod method) {
161+
String methodName = method.getName();
162+
boolean isPublic = method.getModifierList().hasModifierProperty(PsiModifier.PUBLIC);
163+
boolean isStartsWithGet = methodName.startsWith("get");
164+
boolean isPropertyExist = false;
165+
166+
if (isStartsWithGet) {
167+
isPropertyExist = hasField(method, type);
168+
}
169+
if (!isPublic) {
170+
messageKey = "MapKeyAnnotationsInvalidMethodAccessSpecifier";
171+
code = PersistenceConstants.DIAGNOSTIC_CODE_INVALID_ACCESS_SPECIFIER;
172+
} else if (!isStartsWithGet) {
173+
messageKey = "MapKeyAnnotationsOnInvalidMethod";
174+
code = PersistenceConstants.DIAGNOSTIC_CODE_INVALID_METHOD_NAME;
175+
} else if (!isPropertyExist) {
176+
messageKey = "MapKeyAnnotationsFieldNotFound";
177+
code = PersistenceConstants.DIAGNOSTIC_CODE_FIELD_NOT_EXIST;
178+
}
179+
if (messageKey != null) {
180+
diagnostics.add(createDiagnostic(fieldOrProperty, unit, Messages.getMessage(messageKey),
181+
code, null, DiagnosticSeverity.Warning));
182+
}
183+
}
184+
}
185+
186+
private boolean hasField(PsiMethod method, PsiClass type) {
187+
String methodName = method.getName();
188+
// Exclude 'get' from method name and decapitalize the first letter
189+
String expectedFieldName = (methodName.startsWith("get") && methodName.length() > 3) ? Introspector.decapitalize(methodName.substring(3)) : null;
190+
PsiField expectedField = StringUtils.isNotBlank(expectedFieldName) ? type.findFieldByName(expectedFieldName, false) : null;
191+
return expectedField != null;
192+
}
113193
}

src/main/resources/io/openliberty/tools/intellij/lsp4jakarta/messages/messages.properties

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# /*******************************************************************************
2-
# * Copyright (c) 2022, 2024 IBM Corporation and others.
2+
# * Copyright (c) 2022, 2025 IBM Corporation and others.
33
# *
44
# * This program and the accompanying materials are made available under the
55
# * terms of the Eclipse Public License v. 2.0 which is available at
@@ -145,6 +145,11 @@ AddNoArgPublicConstructor = Add a no-arg public constructor to this class
145145
MapKeyAnnotationsNotOnSameField = @MapKeyClass and @MapKey annotations cannot be used on the same field or property.
146146
MultipleMapKeyJoinColumnMethod = A method with multiple @MapKeyJoinColumn annotations must specify both the name and referencedColumnName attributes in the corresponding @MapKeyJoinColumn annotations.
147147
MultipleMapKeyJoinColumnField = A field with multiple @MapKeyJoinColumn annotations must specify both the name and referencedColumnName attributes in the corresponding @MapKeyJoinColumn annotations.
148+
MapKeyAnnotationsInvalidMethodAccessSpecifier = Method is not public and may not be accessible as expected.
149+
MapKeyAnnotationsFieldNotFound = Method has no matching field name.
150+
MapKeyAnnotationsOnInvalidMethod = This method does not conform to persistent property getter naming conventions.
151+
MapKeyAnnotationsTypeOfField = `{0}` annotation can only be applied to fields of type java.util.Map.
152+
MapKeyAnnotationsReturnTypeOfMethod = `{0}` annotation can only be applied to methods with a return type of java.util.Map.
148153

149154
# CompleteFilterAnnotationQuickFix
150155
AddTheAttributeTo = Add the `{0}` attribute to {1}

src/test/java/io/openliberty/tools/intellij/lsp4jakarta/it/persistence/JakartaPersistenceTest.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2021, 2024 IBM Corporation and others.
2+
* Copyright (c) 2021, 2025 IBM Corporation and others.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -310,4 +310,55 @@ public void removeFinalModifiers() throws Exception {
310310

311311
assertJavaCodeAction(codeActionParams5, utils, ca5);
312312
}
313+
314+
@Test
315+
public void testMethodOrFieldType() throws Exception {
316+
Module module = createMavenModule(new File("src/test/resources/projects/maven/jakarta-sample"));
317+
IPsiUtils utils = PsiUtilsLSImpl.getInstance(getProject());
318+
319+
VirtualFile javaFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(ModuleUtilCore.getModuleDirPath(module)
320+
+ "/src/main/java/io/openliberty/sample/jakarta/persistence/MapKeyAnnotationsType.java");
321+
String uri = VfsUtilCore.virtualToIoFile(javaFile).toURI().toString();
322+
323+
JakartaJavaDiagnosticsParams diagnosticsParams = new JakartaJavaDiagnosticsParams();
324+
diagnosticsParams.setUris(Arrays.asList(uri));
325+
326+
Diagnostic d1 = d(27, 19, 25,
327+
"`@MapKey` annotation can only be applied to methods with a return type of java.util.Map.",
328+
DiagnosticSeverity.Error, "jakarta-persistence", "InvalidReturnTypeOfMethod");
329+
330+
Diagnostic d2 = d(13, 11, 15,
331+
"`@MapKey` annotation can only be applied to fields of type java.util.Map.",
332+
DiagnosticSeverity.Error, "jakarta-persistence", "InvalidTypeOfField");
333+
334+
assertJavaDiagnostics(diagnosticsParams, utils, d1, d2);
335+
}
336+
337+
@Test
338+
public void testAccessorAndNamingConventions() throws Exception {
339+
Module module = createMavenModule(new File("src/test/resources/projects/maven/jakarta-sample"));
340+
IPsiUtils utils = PsiUtilsLSImpl.getInstance(getProject());
341+
342+
VirtualFile javaFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(ModuleUtilCore.getModuleDirPath(module)
343+
+ "/src/main/java/io/openliberty/sample/jakarta/persistence/MapKeyAnnotationsGetterConvention.java");
344+
String uri = VfsUtilCore.virtualToIoFile(javaFile).toURI().toString();
345+
346+
JakartaJavaDiagnosticsParams diagnosticsParams = new JakartaJavaDiagnosticsParams();
347+
diagnosticsParams.setUris(Arrays.asList(uri));
348+
349+
Diagnostic d1 = d(37, 33, 41,
350+
"Method is not public and may not be accessible as expected.",
351+
DiagnosticSeverity.Warning, "jakarta-persistence", "InvalidMethodAccessSpecifier");
352+
353+
Diagnostic d2 = d(42, 33, 41,
354+
"This method does not conform to persistent property getter naming conventions.",
355+
DiagnosticSeverity.Warning, "jakarta-persistence", "InvalidMethodName");
356+
357+
Diagnostic d3 = d(47, 32, 42,
358+
"Method has no matching field name.",
359+
DiagnosticSeverity.Warning, "jakarta-persistence", "InvalidMapKeyAnnotationsFieldNotFound");
360+
361+
assertJavaDiagnostics(diagnosticsParams, utils, d1, d2, d3);
362+
}
363+
313364
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.openliberty.sample.jakarta.persistence;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
import jakarta.persistence.MapKey;
7+
import jakarta.persistence.MapKeyClass;
8+
9+
public class MapKeyAnnotationsGetterConvention {
10+
11+
Integer age;
12+
13+
14+
String name;
15+
16+
Map<Integer, String> place;
17+
18+
Map<Integer, String> gender;
19+
20+
Map<Integer, String> testMap = new HashMap<>();
21+
22+
23+
@MapKeyClass(Map.class)
24+
public Map<Integer, String> getTestMap() {
25+
return this.testMap;
26+
}
27+
28+
29+
public Integer getAge() {
30+
return this.age;
31+
}
32+
33+
public String getName() {
34+
return this.name;
35+
}
36+
37+
@MapKeyClass(Map.class)
38+
private Map<Integer, String> getPlace() {
39+
return this.place;
40+
}
41+
42+
@MapKey()
43+
public Map<Integer, String> geGender() {
44+
return null;
45+
}
46+
47+
@MapKey()
48+
public Map<Integer, String> getPerform() {
49+
return null;
50+
}
51+
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.openliberty.sample.jakarta.persistence;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
import jakarta.persistence.MapKey;
7+
import jakarta.persistence.MapKeyClass;
8+
9+
public class MapKeyAnnotationsType {
10+
11+
Integer age;
12+
13+
@MapKey()
14+
String name;
15+
16+
Map<Integer, String> place;
17+
18+
Map<Integer, String> gender;
19+
20+
Map<Integer, String> testMap = new HashMap<>();
21+
22+
23+
public Map<Integer, String> getTestMap() {
24+
return this.testMap;
25+
}
26+
27+
@MapKey()
28+
public Integer getAge() {
29+
return this.age;
30+
}
31+
32+
public String getName() {
33+
return this.name;
34+
}
35+
36+
private Map<Integer, String> getPlace() {
37+
return this.place;
38+
}
39+
40+
}

0 commit comments

Comments
 (0)