This is an automated email from the ASF dual-hosted git repository.
tkobayas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-tools.git
The following commit(s) were added to refs/heads/main by this push:
new 38210475da4 incubator-kie-tools#3966: [drl-vscode-extension] static
members after a type name (#3999)
38210475da4 is described below
commit 38210475da496e9524cbe514b3a63fff4555d792
Author: Daniel Clark <[email protected]>
AuthorDate: Tue Sep 29 02:34:52 2026 +0200
incubator-kie-tools#3966: [drl-vscode-extension] static members after a
type name (#3999)
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
.../org/drools/completion/ClassMemberIndex.java | 105 +++++++++-
.../org/drools/completion/DRLCompletionHelper.java | 76 +++++++-
.../drools/completion/DRLDeclaredTypeParser.java | 2 +-
.../java/org/drools/completion/DRLHoverHelper.java | 145 +++++++++++---
.../java/org/drools/completion/DeclaredType.java | 12 ++
.../org/drools/completion/JavaMemberSource.java | 12 ++
.../java/org/drools/completion/JavaSourceType.java | 22 ++-
.../org/drools/completion/JavaSourceTypeIndex.java | 16 +-
.../drools/completion/JavaSourceTypeParser.java | 216 ++++++++++++++-------
.../drools/completion/ClassMemberIndexTest.java | 75 +++++++
.../drools/completion/DRLCompletionHelperTest.java | 182 +++++++++++++++++
.../completion/DRLDeclaredTypeParserTest.java | 2 +
.../org/drools/completion/DRLHoverHelperTest.java | 163 ++++++++++++++--
.../drools/completion/JavaSourceTypeIndexTest.java | 59 ++++++
.../completion/JavaSourceTypeParserTest.java | 135 +++++++++++++
.../org/drools/completion/fixtures/Limits.java | 30 +++
.../org/drools/completion/fixtures/Rounding.java | 58 ++++++
17 files changed, 1179 insertions(+), 131 deletions(-)
diff --git
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/ClassMemberIndex.java
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/ClassMemberIndex.java
index 31eaa94fbdc..8dc6e681626 100644
---
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/ClassMemberIndex.java
+++
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/ClassMemberIndex.java
@@ -245,16 +245,8 @@ public final class ClassMemberIndex implements
AutoCloseable {
if (!Modifier.isPublic(ctor.getModifiers())) {
continue;
}
- StringBuilder signature = new
StringBuilder(clazz.getSimpleName()).append('(');
- Class<?>[] params = ctor.getParameterTypes();
- for (int i = 0; i < params.length; i++) {
- if (i > 0) {
- signature.append(", ");
- }
- signature.append(params[i].getSimpleName());
- }
- signature.append(')');
- out.add(signature.toString());
+ out.add(clazz.getSimpleName() + "("
+ + parameterList(ctor.getParameterTypes(),
ctor.isVarArgs()) + ")");
}
return Collections.unmodifiableList(out);
} catch (Throwable t) {
@@ -263,6 +255,99 @@ public final class ClassMemberIndex implements
AutoCloseable {
}
}
+ /**
+ * Parameter types as simple names, a varargs tail as {@code T...} — the
+ * shape the source parser reports, so a signature reads the same before
and
+ * after a build.
+ */
+ private static String parameterList(Class<?>[] params, boolean varArgs) {
+ StringBuilder out = new StringBuilder();
+ for (int i = 0; i < params.length; i++) {
+ if (i > 0) {
+ out.append(", ");
+ }
+ if (varArgs && i == params.length - 1) {
+
out.append(params[i].getComponentType().getSimpleName()).append("...");
+ } else {
+ out.append(params[i].getSimpleName());
+ }
+ }
+ return out.toString();
+ }
+
+ /**
+ * Public static fields of {@code fqcn} as name and type — enum constants
+ * included, since they are public static fields of their own enum — or the
+ * {@link #fallback} source's when the class can't load, or empty when
+ * neither knows the type.
+ *
+ * <p>Answers for the {@code Type.NAME} position, so it is deliberately
+ * disjoint from {@link #membersOf}: that view is instance members only.
+ * Only a field's declared type is read, never its value, so this cannot
+ * run a static initializer. Not cached, matching
+ * {@link #constructorsOf} — the static view is consulted on hover and on
+ * completion after a dot, not per pattern per request.
+ */
+ public List<Field> staticFieldsOf(String fqcn) {
+ if (fqcn == null || fqcn.isEmpty()) {
+ return Collections.emptyList();
+ }
+ Class<?> clazz = tryLoad(fqcn);
+ if (clazz == null) {
+ JavaMemberSource f = fallback;
+ return f != null ? f.staticFieldsOf(fqcn) :
Collections.emptyList();
+ }
+ try {
+ List<Field> out = new ArrayList<>();
+ for (java.lang.reflect.Field field : clazz.getFields()) {
+ if (Modifier.isStatic(field.getModifiers())) {
+ out.add(new Field(field.getName(),
field.getType().getSimpleName(), null,
+ field.isEnumConstant() ?
Field.Origin.ENUM_CONSTANT
+ :
Field.Origin.FIELD));
+ }
+ }
+ return Collections.unmodifiableList(out);
+ } catch (Throwable t) {
+ logger.log(Level.FINE, "Failed to reflect static fields of " +
fqcn, t);
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Public static method signatures of {@code fqcn} —
+ * {@code name(ParamSimple, ParamSimple) : Return}, the shape
+ * {@link #constructorsOf} uses plus the return type a constructor has no
+ * need of — or the {@link #fallback} source's when the
+ * class can't load, or empty when neither knows the type. Inherited public
+ * statics are included, matching {@code getFields()} above and Java
itself,
+ * which permits reaching an inherited static through a subtype's name. Not
+ * cached, as above.
+ */
+ public List<String> staticMethodsOf(String fqcn) {
+ if (fqcn == null || fqcn.isEmpty()) {
+ return Collections.emptyList();
+ }
+ Class<?> clazz = tryLoad(fqcn);
+ if (clazz == null) {
+ JavaMemberSource f = fallback;
+ return f != null ? f.staticMethodsOf(fqcn) :
Collections.emptyList();
+ }
+ try {
+ List<String> out = new ArrayList<>();
+ for (Method m : clazz.getMethods()) {
+ if (!Modifier.isStatic(m.getModifiers()) ||
!Modifier.isPublic(m.getModifiers())) {
+ continue;
+ }
+ out.add(m.getName() + "(" +
parameterList(m.getParameterTypes(), m.isVarArgs())
+ + ") : " + m.getReturnType().getSimpleName());
+ }
+ return Collections.unmodifiableList(out);
+ } catch (Throwable t) {
+ logger.log(Level.FINE, "Failed to reflect static methods of " +
fqcn, t);
+ return Collections.emptyList();
+ }
+ }
+
/**
* Attempts to load {@code fqcn} without running static initializers.
* Returns {@code null} (rather than throwing) when the loader is absent
diff --git
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLCompletionHelper.java
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLCompletionHelper.java
index 9523dac947e..c9bb0c6e712 100644
---
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLCompletionHelper.java
+++
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLCompletionHelper.java
@@ -313,11 +313,18 @@ public class DRLCompletionHelper {
String head = chain[0];
String rootType;
int firstFieldSegment = 1;
- if (head.startsWith("$")) {
+ boolean typeReference = false;
+ int fqcnEnd = DRLHoverHelper.fqcnPrefixEnd(chain, classIndex);
+ if (fqcnEnd >= 1) {
+ rootType = String.join(".", Arrays.copyOfRange(chain, 0, fqcnEnd +
1));
+ firstFieldSegment = fqcnEnd + 1;
+ typeReference = true;
+ } else if (head.startsWith("$")) {
rootType = LhsBindingResolver.resolveAt(text,
DRLHoverHelper.positionToOffset(text, caret), typeIndex)
.get(head.substring(1));
} else if (!head.isEmpty() && Character.isUpperCase(head.charAt(0))) {
rootType = head;
+ typeReference = true;
} else {
// A bare lower-case head is a field of the pattern the caret is
in.
rootType = enclosingPatternTypeFromText(text,
DRLHoverHelper.positionToOffset(text, caret));
@@ -329,6 +336,36 @@ public class DRLCompletionHelper {
return null;
}
+ // After a type name Java permits only statics — the one set the
instance
+ // view cannot legally offer. A DRL declare's only statics are its enum
+ // constants. Only the first hop is static; past it, instance members
+ // resume, because a constant is an ordinary value of its own type.
+ if (typeReference) {
+ List<Field> statics;
+ List<String> staticMethods;
+ DeclaredType declared = typeIndex.get(rootType);
+ if (declared != null) {
+ statics = declared.enumConstants();
+ staticMethods = List.of();
+ } else {
+ String fqcn = resolveFqcn(rootType, simpleNameOf(rootType),
compilationUnit, classIndex);
+ if (fqcn == null) {
+ return List.of();
+ }
+ statics = memberIndex.staticFieldsOf(fqcn);
+ staticMethods = memberIndex.staticMethodsOf(fqcn);
+ }
+ if (firstFieldSegment >= chain.length) {
+ return staticItems(statics, staticMethods);
+ }
+ String hopType = typeOfStatic(statics, chain[firstFieldSegment]);
+ if (hopType == null) {
+ return List.of();
+ }
+ rootType = hopType;
+ firstFieldSegment++;
+ }
+
// Kept qualified. A pattern head written as a fully-qualified name is
// how an author disambiguates a simple name that collides on the
// classpath, and resolveFqcn returns a dotted name as-is, so dropping
@@ -393,11 +430,46 @@ public class DRLCompletionHelper {
return null;
}
+ /** Simple name of a possibly-qualified type name. */
+ private static String simpleNameOf(String typeName) {
+ return typeName.substring(typeName.lastIndexOf('.') + 1);
+ }
+
+ /** The declared type of the static field named {@code name}, or {@code
null}. */
+ private static String typeOfStatic(List<Field> statics, String name) {
+ for (Field candidate : statics) {
+ if (candidate.name.equals(name)) {
+ return candidate.type;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Items for the {@code Type.NAME} position: static fields carrying their
+ * type, then static methods carrying their signature. A method's label is
+ * its bare name so it completes to something callable, with the signature
in
+ * the detail — two overloads therefore share a label and differ in detail.
+ */
+ private static List<CompletionItem> staticItems(List<Field> staticFields,
List<String> staticMethods) {
+ List<CompletionItem> items = new ArrayList<>(fieldItems(staticFields));
+ for (String signature : staticMethods) {
+ int paren = signature.indexOf('(');
+ CompletionItem item = new CompletionItem();
+ item.setLabel(paren < 0 ? signature : signature.substring(0,
paren));
+ item.setInsertText(item.getLabel());
+ item.setDetail(signature);
+ item.setKind(CompletionItemKind.Method);
+ items.add(item);
+ }
+ return items;
+ }
+
/** Field items for a type name: DRL declares win, then the classpath. */
private static List<CompletionItem> memberItemsOfType(String typeName,
Map<String, DeclaredType> typeIndex,
DRL10Parser.CompilationUnitContext compilationUnit,
ClassIndex
classIndex, ClassMemberIndex memberIndex) {
- String simple = typeName.substring(typeName.lastIndexOf('.') + 1);
+ String simple = simpleNameOf(typeName);
DeclaredType declared = typeIndex.get(simple);
if (declared != null) {
return
fieldItems(DRLDeclaredTypeParser.fieldsIncludingInherited(declared, typeIndex));
diff --git
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLDeclaredTypeParser.java
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLDeclaredTypeParser.java
index 1506c92827a..9f70f69fba1 100644
---
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLDeclaredTypeParser.java
+++
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLDeclaredTypeParser.java
@@ -200,7 +200,7 @@ public final class DRLDeclaredTypeParser {
for (DRL10Parser.EnumerativeContext enumerative :
ctx.enumeratives().enumerative()) {
if (enumerative.drlIdentifier() != null) {
fields.add(new
Field(enumerative.drlIdentifier().getText(), name,
- extractEnumArgs(enumerative)));
+ extractEnumArgs(enumerative),
Field.Origin.ENUM_CONSTANT));
}
}
}
diff --git
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLHoverHelper.java
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLHoverHelper.java
index 2a7e6bfb18a..38a2ae6a85b 100644
---
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLHoverHelper.java
+++
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLHoverHelper.java
@@ -136,8 +136,7 @@ public final class DRLHoverHelper {
String boundFqcn = DRLCompletionHelper.resolveFqcn(
boundType, boundType, compilationUnit, classIndex);
if (boundFqcn != null) {
- return markdown(renderJavaType(boundType, boundFqcn,
memberIndex.membersOf(boundFqcn),
- memberIndex.constructorsOf(boundFqcn)));
+ return markdown(renderJavaType(boundType, boundFqcn,
memberIndex));
}
// Nothing to describe beyond the name: a primitive has no
class
// to load and no members, and the type is the useful part
anyway.
@@ -162,8 +161,7 @@ public final class DRLHoverHelper {
// members — knowing the FQN (e.g. java.lang.Object) is still
useful.
String fqcn = DRLCompletionHelper.resolveFqcn(word, word,
compilationUnit, classIndex);
if (fqcn != null) {
- return markdown(renderJavaType(word, fqcn,
memberIndex.membersOf(fqcn),
- memberIndex.constructorsOf(fqcn)));
+ return markdown(renderJavaType(word, fqcn, memberIndex));
}
// 6. Documented function/query/global, last of all. The doc-comment
@@ -258,6 +256,10 @@ public final class DRLHoverHelper {
Path documentPath, Map<Path, String>
openFiles) {
String runningType = null;
int start = 0;
+ // True while the running type came from a type *name* rather than a
+ // value — the only position where Java permits statics. Cleared after
+ // one hop, since a constant is an ordinary value of its own type.
+ boolean fromTypeRef = false;
// A chain can open with a fully-qualified type name, whose leading
// segments are package names that resolve to nothing on their own
@@ -270,10 +272,10 @@ public final class DRLHoverHelper {
if (chain.hoveredIndex <= fqcnEnd) {
// Anywhere inside the qualified name describes that type.
String simple = chain.segments[fqcnEnd];
- return markdown(renderJavaType(simple, fqcn,
memberIndex.membersOf(fqcn),
- memberIndex.constructorsOf(fqcn)));
+ return markdown(renderJavaType(simple, fqcn, memberIndex));
}
runningType = fqcn;
+ fromTypeRef = true;
start = fqcnEnd + 1;
}
@@ -304,15 +306,16 @@ public final class DRLHoverHelper {
currentDocTypes, text, documentPath,
openFiles));
}
runningType = segment;
+ fromTypeRef = true;
} else {
String fqcn = DRLCompletionHelper.resolveFqcn(
segment, segment, parsed.compilationUnit,
classIndex);
if (fqcn != null) {
if (hovered) {
- return markdown(renderJavaType(segment, fqcn,
memberIndex.membersOf(fqcn),
- memberIndex.constructorsOf(fqcn)));
+ return markdown(renderJavaType(segment, fqcn,
memberIndex));
}
runningType = fqcn;
+ fromTypeRef = true;
} else {
Integer nodeIndex = parsed.tokenIndexAt(position);
String patternType = nodeIndex == null ? null
@@ -336,13 +339,41 @@ public final class DRLHoverHelper {
}
} else {
DeclaredType declared = typeIndex.get(simpleName(runningType));
- if (declared != null && declared.isEnum &&
isEnumConstant(declared, segment)) {
+ if (declared != null && isEnumConstant(declared, segment)) {
if (hovered) {
return markdown(fencedHeader(declared.name + "." +
segment)
+ renderDeclaredHover(declared, typeIndex,
currentDocTypes,
text, documentPath,
openFiles));
}
// Running type stays the enum: the constant is an
instance of it.
+ fromTypeRef = false;
+ } else if (fromTypeRef) {
+ fromTypeRef = false;
+ if (declared != null) {
+ // A declare's only statics are its enum constants,
handled above.
+ return null;
+ }
+ // Directly after a classpath type name, so the segment is
a static.
+ // runningType is the resolved FQCN at this point.
+ Field staticField =
findByName(memberIndex.staticFieldsOf(runningType), segment);
+ if (staticField != null) {
+ if (hovered) {
+ return markdown(renderStatic(staticField.name,
staticField.type,
+ "field", simpleName(runningType)));
+ }
+ runningType = staticField.type;
+ } else {
+ String signature =
findSignature(memberIndex.staticMethodsOf(runningType), segment);
+ if (signature == null) {
+ return null;
+ }
+ if (hovered) {
+ return markdown(renderStatic(signature, null,
"method",
+ simpleName(runningType)));
+ }
+ // A call's result type is not modelled, so the walk
stops.
+ return null;
+ }
} else {
Field field = findField(runningType, segment, typeIndex,
parsed.compilationUnit,
classIndex, memberIndex);
@@ -366,7 +397,7 @@ public final class DRLHoverHelper {
* {@code com.example.Order.status} prefers the type {@code
com.example.Order}
* over a shorter accidental match.
*/
- private static int fqcnPrefixEnd(String[] segments, ClassIndex classIndex)
{
+ static int fqcnPrefixEnd(String[] segments, ClassIndex classIndex) {
for (int end = segments.length - 1; end >= 1; end--) {
String simple = segments[end];
if (simple.isEmpty()) {
@@ -389,10 +420,8 @@ public final class DRLHoverHelper {
return sb.toString();
}
- /** True when {@code name} is a constant of {@code enumType} (a field
typed as the enum). */
- private static boolean isEnumConstant(DeclaredType enumType, String name) {
- return enumType.fields.stream()
- .anyMatch(field -> name.equals(field.name) &&
enumType.name.equals(field.type));
+ private static boolean isEnumConstant(DeclaredType declared, String name) {
+ return declared.enumConstants().stream().anyMatch(field ->
name.equals(field.name));
}
/** Header {@code $x : Type} followed by the bound type's details. */
@@ -410,8 +439,7 @@ public final class DRLHoverHelper {
String fqcn = DRLCompletionHelper.resolveFqcn(
typeName, typeName, parsed.compilationUnit, classIndex);
if (fqcn != null) {
- return header + renderJavaType(typeName, fqcn,
memberIndex.membersOf(fqcn),
- memberIndex.constructorsOf(fqcn));
+ return header + renderJavaType(typeName, fqcn, memberIndex);
}
return header.stripTrailing();
}
@@ -490,6 +518,41 @@ public final class DRLHoverHelper {
return "**" + field.name + "** : `" + field.type + "`\n\nField of `" +
owner + "`";
}
+ /**
+ * Hover body for a static, named as such so it reads apart from a fact
+ * property. {@code type} is {@code null} for a method, whose {@code name}
+ * already carries its signature.
+ */
+ private static String renderStatic(String name, String type, String kind,
String owner) {
+ return "**" + name + "**" + (type == null ? "" : " : `" + type + "`")
+ + "\n\nStatic " + kind + " of `" + owner + "`";
+ }
+
+ /** The member of {@code fields} called {@code name}, or {@code null}. */
+ private static Field findByName(List<Field> fields, String name) {
+ for (Field field : fields) {
+ if (name.equals(field.name)) {
+ return field;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * The first signature in {@code signatures} whose method name is
+ * {@code name}, or {@code null}. Overloads are not distinguished — the
+ * chain carries no argument list to select by.
+ */
+ private static String findSignature(List<String> signatures, String name) {
+ for (String signature : signatures) {
+ int paren = signature.indexOf('(');
+ if (paren > 0 && signature.regionMatches(0, name, 0, paren) &&
paren == name.length()) {
+ return signature;
+ }
+ }
+ return null;
+ }
+
private static String simpleName(String typeName) {
return typeName.substring(typeName.lastIndexOf('.') + 1);
}
@@ -572,7 +635,7 @@ public final class DRLHoverHelper {
}
sb.append('\n');
for (Field field : dt.fields) {
- if (dt.isEnum && dt.name.equals(field.type)) {
+ if (field.origin == Field.Origin.ENUM_CONSTANT) {
sb.append(" ").append(field.name);
if (field.args != null) {
sb.append('(').append(field.args).append(')');
@@ -594,35 +657,63 @@ public final class DRLHoverHelper {
return sb.toString();
}
+ /**
+ * The type's own hover. {@code statics} feeds the Constants section — its
+ * enum constants and its {@code public static} fields alike, since
+ * {@code membersOf} carries neither: an instance view cannot describe what
+ * is reachable as {@code Type.NAME}. A type whose whole API is static
+ * (a constants holder, {@code java.lang.Math}) would otherwise render as a
+ * bare header.
+ */
+ private static String renderJavaType(String simpleName, String fqcn,
ClassMemberIndex memberIndex) {
+ return renderJavaType(simpleName, fqcn, memberIndex.membersOf(fqcn),
+ memberIndex.staticFieldsOf(fqcn),
memberIndex.staticMethodsOf(fqcn),
+ memberIndex.constructorsOf(fqcn));
+ }
+
private static String renderJavaType(String simpleName, String fqcn,
List<Field> members,
+ List<Field> statics, List<String>
staticMethods,
List<String> constructors) {
StringBuilder sb = new StringBuilder();
sb.append("**").append(simpleName).append("** —
`").append(fqcn).append("`\n");
- appendMemberSection(sb, "Constants", members,
Field.Origin.ENUM_CONSTANT);
+ appendMemberSection(sb, "Constants", statics, null);
appendMemberSection(sb, "Fields", members, Field.Origin.FIELD);
appendMemberSection(sb, "Getters", members, Field.Origin.GETTER);
- if (!constructors.isEmpty()) {
- sb.append("\n\n**Constructors**");
- for (String signature : constructors) {
- sb.append("\n- `").append(signature).append('`');
- }
- }
+ appendSignatureSection(sb, "Static methods", staticMethods);
+ appendSignatureSection(sb, "Constructors", constructors);
return sb.toString();
}
- /** Appends {@code title}'s section for {@code members} of the given
{@code origin}, or nothing when none match. */
+ /** Appends {@code title}'s section of signature strings, or nothing when
empty. */
+ private static void appendSignatureSection(StringBuilder sb, String title,
+ List<String> signatures) {
+ if (signatures.isEmpty()) {
+ return;
+ }
+ sb.append("\n\n**").append(title).append("**");
+ for (String signature : signatures) {
+ sb.append("\n- `").append(signature).append('`');
+ }
+ }
+
+ /**
+ * Appends {@code title}'s section for the members of {@code origin} — or
for
+ * every member when {@code origin} is {@code null} — writing nothing when
+ * none match. An enum constant renders bare, everything else as
+ * {@code name : type}.
+ */
private static void appendMemberSection(StringBuilder sb, String title,
List<Field> members,
Field.Origin origin) {
boolean headerWritten = false;
for (Field member : members) {
- if (member.origin != origin) {
+ if (origin != null && member.origin != origin) {
continue;
}
if (!headerWritten) {
sb.append("\n\n**").append(title).append("**");
headerWritten = true;
}
- if (origin == Field.Origin.ENUM_CONSTANT) {
+ if (member.origin == Field.Origin.ENUM_CONSTANT) {
sb.append("\n- ").append(member.name);
if (member.args != null) {
sb.append(" (").append(member.args).append(')');
diff --git
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DeclaredType.java
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DeclaredType.java
index 344f4684cab..b2f810686af 100644
---
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DeclaredType.java
+++
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DeclaredType.java
@@ -19,6 +19,7 @@
package org.drools.completion;
+import java.util.ArrayList;
import java.util.List;
/** A type declared in DRL via a {@code declare} block. */
@@ -51,4 +52,15 @@ public class DeclaredType {
this.nameCol = nameCol;
this.extendsName = extendsName;
}
+
+ /** The constants of a declared enum, or an empty list for a non-enum
declare. */
+ List<Field> enumConstants() {
+ List<Field> constants = new ArrayList<>();
+ for (Field field : fields) {
+ if (field.origin == Field.Origin.ENUM_CONSTANT) {
+ constants.add(field);
+ }
+ }
+ return constants;
+ }
}
diff --git
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaMemberSource.java
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaMemberSource.java
index d07206916ee..59508fc0c3d 100644
---
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaMemberSource.java
+++
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaMemberSource.java
@@ -36,6 +36,14 @@ import java.util.Set;
* supertypes as fully-qualified names where resolvable within the source
* index — unresolvable supertypes are omitted, never guessed at — empty when
* unknown; {@link #constructorsOf} returns signatures, empty when unknown.
+ *
+ * <p>{@link #staticFieldsOf} and {@link #staticMethodsOf} answer for the
+ * {@code Type.NAME} position, where Java permits only statics, while
+ * {@link #membersOf} is the instance (fact-property) view. A static is not a
+ * fact property, so the two share nothing — except an enum's constants, which
+ * are public static fields of their enum and also how a rule names its values,
+ * so they appear in both, as reflection reports them. Both are empty when the
+ * type is unknown.
*/
public interface JavaMemberSource {
@@ -46,4 +54,8 @@ public interface JavaMemberSource {
List<String> supertypesOf(String fqcn);
List<String> constructorsOf(String fqcn);
+
+ List<Field> staticFieldsOf(String fqcn);
+
+ List<String> staticMethodsOf(String fqcn);
}
diff --git
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaSourceType.java
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaSourceType.java
index c7f23582c9c..cfa34bfe2be 100644
---
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaSourceType.java
+++
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaSourceType.java
@@ -23,8 +23,9 @@ import java.util.List;
/**
* A Java type parsed from {@code .java} source (top-level class, enum,
interface,
- * or record). Carries the members, constructor signatures, supertype simple
- * names, and the declaration's source position, so the source-typing layer can
+ * or record). Carries the instance members, the statics, constructor
signatures,
+ * supertype simple names, and the declaration's source position, so the
+ * source-typing layer can
* feed completion/hover/lint/definition before a compile exists. Immutable.
*/
public final class JavaSourceType {
@@ -41,19 +42,21 @@ public final class JavaSourceType {
/** Constructor signatures, e.g. {@code "Foo(int, String)"}. */
public final List<String> constructors;
/**
- * Names of the public static fields. Not members — a static is not a fact
- * property — but nameable through {@code Type.NAME}, which is what
- * {@link JavaSourceTypeIndex#memberNames} answers for.
+ * The public static fields, with their types. Not members — a static is
not
+ * a fact property — but reachable through {@code Type.NAME}, which is what
+ * {@link JavaSourceTypeIndex#staticFieldsOf} and
+ * {@link JavaSourceTypeIndex#memberNames} answer for.
*/
- public final List<String> staticFieldNames;
+ public final List<Field> staticFields;
+ public final List<String> staticMethods;
/** 0-based line/column of the type's name token. */
public final int declLine;
public final int declColumn;
JavaSourceType(String fqcn, String simpleName, boolean isEnum, String
extendsSimpleName,
List<String> interfaceSimpleNames, List<Field> members,
- List<String> constructors, List<String> staticFieldNames,
- int declLine, int declColumn) {
+ List<String> constructors, List<Field> staticFields,
+ List<String> staticMethods, int declLine, int declColumn) {
this.fqcn = fqcn;
this.simpleName = simpleName;
this.isEnum = isEnum;
@@ -61,7 +64,8 @@ public final class JavaSourceType {
this.interfaceSimpleNames = List.copyOf(interfaceSimpleNames);
this.members = List.copyOf(members);
this.constructors = List.copyOf(constructors);
- this.staticFieldNames = List.copyOf(staticFieldNames);
+ this.staticFields = List.copyOf(staticFields);
+ this.staticMethods = List.copyOf(staticMethods);
this.declLine = declLine;
this.declColumn = declColumn;
}
diff --git
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaSourceTypeIndex.java
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaSourceTypeIndex.java
index 7c2727c8ff1..1cb8ab77e82 100644
---
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaSourceTypeIndex.java
+++
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaSourceTypeIndex.java
@@ -277,10 +277,24 @@ public final class JavaSourceTypeIndex implements
JavaMemberSource {
return null;
}
Set<String> names = new
LinkedHashSet<>(membersIncludingInherited(fqcn, type).keySet());
- names.addAll(type.staticFieldNames);
+ for (Field staticField : type.staticFields) {
+ names.add(staticField.name);
+ }
return names;
}
+ @Override
+ public List<Field> staticFieldsOf(String fqcn) {
+ JavaSourceType type = byFqcn(fqcn);
+ return type == null ? List.of() : type.staticFields;
+ }
+
+ @Override
+ public List<String> staticMethodsOf(String fqcn) {
+ JavaSourceType type = byFqcn(fqcn);
+ return type == null ? List.of() : type.staticMethods;
+ }
+
/**
* Returns {@code type}'s own members followed by those of its ancestors,
* walked through {@code extendsSimpleName} within this index only, so a
diff --git
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaSourceTypeParser.java
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaSourceTypeParser.java
index ea6eed18a91..9bc0ac59263 100644
---
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaSourceTypeParser.java
+++
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/JavaSourceTypeParser.java
@@ -24,6 +24,7 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Function;
import java.util.logging.Logger;
import org.antlr.v4.runtime.BaseErrorListener;
@@ -31,6 +32,7 @@ import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
+import org.antlr.v4.runtime.tree.TerminalNode;
import org.drools.drl.parser.antlr4.JavaLexer;
import org.drools.drl.parser.antlr4.JavaParser;
@@ -44,8 +46,8 @@ import org.drools.drl.parser.antlr4.JavaParser;
*
* <p>Known limits (acceptable for typing/hover/lint): nested types are not
* indexed; interface member extraction is name-first (fields/constants may be
- * partial); generic type arguments and array dimensions are erased to the raw
- * simple name.
+ * partial); generic type arguments and package prefixes are erased to the raw
+ * simple name, while array dimensions are kept.
*/
public final class JavaSourceTypeParser {
@@ -124,17 +126,18 @@ public final class JavaSourceTypeParser {
List<Field> fields = new ArrayList<>();
List<Field> getters = new ArrayList<>();
List<String> ctors = new ArrayList<>();
- List<String> staticFields = new ArrayList<>();
+ List<Field> staticFields = new ArrayList<>();
+ List<String> staticMethods = new ArrayList<>();
if (cd.classBody() != null) {
collectBodyMembers(cd.classBody().classBodyDeclaration(), fields,
getters, ctors,
- staticFields, simpleName);
+ staticFields, staticMethods, simpleName);
}
Map<String, Field> members = new LinkedHashMap<>();
mergeGettersThenFields(members, getters, fields);
return new JavaSourceType(fqcn(pkg, simpleName), simpleName, false,
extendsName, interfaces,
- new ArrayList<>(members.values()), ctors, staticFields,
+ new ArrayList<>(members.values()), ctors, staticFields,
staticMethods,
declLine(cd.identifier()), declColumn(cd.identifier()));
}
@@ -143,27 +146,32 @@ public final class JavaSourceTypeParser {
List<String> interfaces = (ed.IMPLEMENTS() != null && ed.typeList() !=
null)
? simplifyAll(ed.typeList()) : List.of();
+ List<Field> staticFields = new ArrayList<>();
Map<String, Field> members = new LinkedHashMap<>();
if (ed.enumConstants() != null) {
for (JavaParser.EnumConstantContext ec :
ed.enumConstants().enumConstant()) {
String name = ec.identifier().getText();
String args = ec.arguments() != null ?
argsText(ec.arguments()) : null;
- members.put(name, new Field(name, simpleName, args,
Field.Origin.ENUM_CONSTANT));
+ Field constant = new Field(name, simpleName, args,
Field.Origin.ENUM_CONSTANT);
+ members.put(name, constant);
+ // A constant is also a public static field of its own enum, so
+ // it belongs to the Type.NAME view — as reflection reports it.
+ staticFields.add(constant);
}
}
List<Field> fields = new ArrayList<>();
List<Field> getters = new ArrayList<>();
List<String> ctors = new ArrayList<>();
- List<String> staticFields = new ArrayList<>();
+ List<String> staticMethods = new ArrayList<>();
if (ed.enumBodyDeclarations() != null) {
collectBodyMembers(ed.enumBodyDeclarations().classBodyDeclaration(), fields,
getters, ctors,
- staticFields, simpleName);
+ staticFields, staticMethods, simpleName);
}
mergeGettersThenFields(members, getters, fields);
return new JavaSourceType(fqcn(pkg, simpleName), simpleName, true,
null, interfaces,
- new ArrayList<>(members.values()), ctors, staticFields,
+ new ArrayList<>(members.values()), ctors, staticFields,
staticMethods,
declLine(ed.identifier()), declColumn(ed.identifier()));
}
@@ -172,22 +180,27 @@ public final class JavaSourceTypeParser {
List<String> interfaces = (id.EXTENDS() != null &&
!id.typeList().isEmpty())
? simplifyAll(id.typeList(0)) : List.of();
- List<Field> fields = new ArrayList<>();
+ // An interface field is implicitly public static final, so
+ // collectInterfaceMember's field list is the constant list: it becomes
+ // the static view, not the member view, matching what reflection
+ // reports once the interface is compiled.
+ List<Field> constants = new ArrayList<>();
List<Field> getters = new ArrayList<>();
+ List<String> staticMethods = new ArrayList<>();
if (id.interfaceBody() != null) {
for (JavaParser.InterfaceBodyDeclarationContext ibd :
id.interfaceBody().interfaceBodyDeclaration()) {
try {
- collectInterfaceMember(ibd, fields, getters);
+ collectInterfaceMember(ibd, constants, getters,
staticMethods);
} catch (Exception e) {
logger.fine(() -> "Skipping interface member in " +
simpleName + ": " + e.getMessage());
}
}
}
Map<String, Field> members = new LinkedHashMap<>();
- mergeGettersThenFields(members, getters, fields);
+ mergeGettersThenFields(members, getters, List.of());
return new JavaSourceType(fqcn(pkg, simpleName), simpleName, false,
null, interfaces,
- new ArrayList<>(members.values()), List.of(), List.of(),
+ new ArrayList<>(members.values()), List.of(), constants,
staticMethods,
declLine(id.identifier()), declColumn(id.identifier()));
}
@@ -211,10 +224,28 @@ public final class JavaSourceTypeParser {
members.putIfAbsent(name, new Field(name, type, null,
Field.Origin.GETTER));
ctorTypes.add(type);
}
- String canonicalCtor = simpleName + "(" + String.join(", ", ctorTypes)
+ ")";
+ List<String> ctors = new ArrayList<>();
+ ctors.add(simpleName + "(" + String.join(", ", ctorTypes) + ")");
+
+ List<Field> fields = new ArrayList<>();
+ List<Field> getters = new ArrayList<>();
+ List<String> bodyCtors = new ArrayList<>();
+ List<Field> staticFields = new ArrayList<>();
+ List<String> staticMethods = new ArrayList<>();
+ if (rd.recordBody() != null) {
+ collectBodyMembers(rd.recordBody().classBodyDeclaration(), fields,
getters, bodyCtors,
+ staticFields, staticMethods, simpleName);
+ }
+ mergeGettersThenFields(members, getters, fields);
+ // An explicitly written canonical constructor repeats the one derived
above.
+ for (String ctor : bodyCtors) {
+ if (!ctors.contains(ctor)) {
+ ctors.add(ctor);
+ }
+ }
return new JavaSourceType(fqcn(pkg, simpleName), simpleName, false,
null, interfaces,
- new ArrayList<>(members.values()), List.of(canonicalCtor),
List.of(),
+ new ArrayList<>(members.values()), ctors, staticFields,
staticMethods,
declLine(rd.identifier()), declColumn(rd.identifier()));
}
@@ -241,8 +272,8 @@ public final class JavaSourceTypeParser {
*/
private static void
collectBodyMembers(List<JavaParser.ClassBodyDeclarationContext> decls,
List<Field> fieldsOut, List<Field>
gettersOut,
- List<String> ctorsOut,
List<String> staticFieldsOut,
- String simpleName) {
+ List<String> ctorsOut, List<Field>
staticFieldsOut,
+ List<String> staticMethodsOut,
String simpleName) {
for (JavaParser.ClassBodyDeclarationContext cbd : decls) {
try {
JavaParser.MemberDeclarationContext md =
cbd.memberDeclaration();
@@ -254,24 +285,28 @@ public final class JavaSourceTypeParser {
String type = simplify(fd.typeType());
boolean isStatic = hasStaticModifier(cbd.modifier());
for (JavaParser.VariableDeclaratorContext vd :
fd.variableDeclarators().variableDeclarator()) {
- String name =
vd.variableDeclaratorId().identifier().getText();
- if (isStatic) {
- // Kept by name only: reachable as Type.NAME, but
not a
- // property of a fact, so out of the member list.
- staticFieldsOut.add(name);
- } else {
- fieldsOut.add(new Field(name, type, null,
Field.Origin.FIELD));
- }
+ JavaParser.VariableDeclaratorIdContext id =
vd.variableDeclaratorId();
+ // A static is reachable as Type.NAME but is not a
property
+ // of a fact, so the two views stay disjoint.
+ (isStatic ? staticFieldsOut : fieldsOut).add(new
Field(id.identifier().getText(),
+ type + dimensions(id.LBRACK()), null,
Field.Origin.FIELD));
}
- } else if (md.methodDeclaration() != null &&
isPublicInstanceMember(cbd.modifier())) {
+ } else if (md.methodDeclaration() != null &&
hasPublicModifier(cbd.modifier())) {
JavaParser.MethodDeclarationContext mt =
md.methodDeclaration();
- String property = getterPropertyOf(mt.typeTypeOrVoid(),
mt.identifier(), mt.formalParameters());
- if (property != null) {
- gettersOut.add(new Field(property,
simplify(mt.typeTypeOrVoid().typeType()), null,
- Field.Origin.GETTER));
+ if (hasStaticModifier(cbd.modifier())) {
+
staticMethodsOut.add(signatureOf(mt.identifier().getText(),
mt.formalParameters())
+ + " : " + returnTypeOf(mt.typeTypeOrVoid()) +
dimensions(mt.LBRACK()));
+ } else {
+ String property =
+ getterPropertyOf(mt.typeTypeOrVoid(),
mt.identifier(), mt.formalParameters());
+ if (property != null) {
+ gettersOut.add(new Field(property,
+ simplify(mt.typeTypeOrVoid().typeType()) +
dimensions(mt.LBRACK()), null,
+ Field.Origin.GETTER));
+ }
}
} else if (md.constructorDeclaration() != null &&
hasPublicModifier(cbd.modifier())) {
- ctorsOut.add(constructorSignature(simpleName,
md.constructorDeclaration().formalParameters()));
+ ctorsOut.add(signatureOf(simpleName,
md.constructorDeclaration().formalParameters()));
}
} catch (Exception e) {
logger.fine(() -> "Skipping class member in " + simpleName +
": " + e.getMessage());
@@ -279,40 +314,33 @@ public final class JavaSourceTypeParser {
}
}
- /**
- * A public, non-static member — the shape reflection reports as an
instance
- * member, and so the only shape this view may claim. Statics are excluded
- * rather than modelled separately: parity with the compiled path is what
- * keeps a member from appearing before a build and vanishing after it.
- */
- private static boolean
isPublicInstanceMember(List<JavaParser.ModifierContext> modifiers) {
- return hasPublicModifier(modifiers) && !hasStaticModifier(modifiers);
- }
-
private static boolean hasStaticModifier(List<JavaParser.ModifierContext>
modifiers) {
- for (JavaParser.ModifierContext modifier : modifiers) {
- JavaParser.ClassOrInterfaceModifierContext coim =
modifier.classOrInterfaceModifier();
- if (coim != null && coim.STATIC() != null) {
- return true;
- }
- }
- return false;
+ return hasModifier(modifiers,
JavaParser.ClassOrInterfaceModifierContext::STATIC);
}
- /** True when {@code modifiers} includes {@code public}. */
private static boolean hasPublicModifier(List<JavaParser.ModifierContext>
modifiers) {
+ return hasModifier(modifiers,
JavaParser.ClassOrInterfaceModifierContext::PUBLIC);
+ }
+
+ private static boolean hasModifier(List<JavaParser.ModifierContext>
modifiers,
+
Function<JavaParser.ClassOrInterfaceModifierContext, TerminalNode> token) {
for (JavaParser.ModifierContext modifier : modifiers) {
JavaParser.ClassOrInterfaceModifierContext coim =
modifier.classOrInterfaceModifier();
- if (coim != null && coim.PUBLIC() != null) {
+ if (coim != null && token.apply(coim) != null) {
return true;
}
}
return false;
}
- /** Extracts a {@code constDeclaration} or no-arg getter from one
interface body member, if any. */
+ /**
+ * Sorts one interface body member into a constant, a no-arg getter, or a
+ * static method signature. A static interface method is implicitly public
+ * unless declared {@code private}, in which case reflection omits it.
+ */
private static void
collectInterfaceMember(JavaParser.InterfaceBodyDeclarationContext ibd,
- List<Field> fieldsOut,
List<Field> gettersOut) {
+ List<Field> fieldsOut,
List<Field> gettersOut,
+ List<String> staticMethodsOut)
{
JavaParser.InterfaceMemberDeclarationContext imd =
ibd.interfaceMemberDeclaration();
if (imd == null) {
return; // bare ';'
@@ -321,20 +349,51 @@ public final class JavaSourceTypeParser {
JavaParser.ConstDeclarationContext cdecl = imd.constDeclaration();
String type = simplify(cdecl.typeType());
for (JavaParser.ConstantDeclaratorContext decl :
cdecl.constantDeclarator()) {
- String name = decl.identifier().getText();
- fieldsOut.add(new Field(name, type, null, Field.Origin.FIELD));
+ fieldsOut.add(new Field(decl.identifier().getText(), type +
dimensions(decl.LBRACK()), null,
+ Field.Origin.FIELD));
}
} else if (imd.interfaceMethodDeclaration() != null) {
- JavaParser.InterfaceCommonBodyDeclarationContext body =
-
imd.interfaceMethodDeclaration().interfaceCommonBodyDeclaration();
+ JavaParser.InterfaceMethodDeclarationContext method =
imd.interfaceMethodDeclaration();
+ JavaParser.InterfaceCommonBodyDeclarationContext body =
method.interfaceCommonBodyDeclaration();
+ if (isStaticInterfaceMethod(ibd, method)) {
+ if (!hasModifier(ibd.modifier(),
JavaParser.ClassOrInterfaceModifierContext::PRIVATE)) {
+
staticMethodsOut.add(signatureOf(body.identifier().getText(),
body.formalParameters())
+ + " : " + returnTypeOf(body.typeTypeOrVoid()) +
dimensions(body.LBRACK()));
+ }
+ return;
+ }
String property = getterPropertyOf(body.typeTypeOrVoid(),
body.identifier(), body.formalParameters());
if (property != null) {
- gettersOut.add(new Field(property,
simplify(body.typeTypeOrVoid().typeType()), null,
+ gettersOut.add(new Field(property,
+ simplify(body.typeTypeOrVoid().typeType()) +
dimensions(body.LBRACK()), null,
Field.Origin.GETTER));
}
}
}
+ /** The {@code []} pairs a declarator carries after its name, which Java
adds to the declared type. */
+ private static String dimensions(List<TerminalNode> brackets) {
+ return "[]".repeat(brackets.size());
+ }
+
+ /**
+ * The grammar accepts {@code static} on an interface method both as a
+ * general {@code modifier} and as an {@code interfaceMethodModifier}, so
+ * both are checked.
+ */
+ private static boolean
isStaticInterfaceMethod(JavaParser.InterfaceBodyDeclarationContext ibd,
+
JavaParser.InterfaceMethodDeclarationContext method) {
+ if (hasStaticModifier(ibd.modifier())) {
+ return true;
+ }
+ for (JavaParser.InterfaceMethodModifierContext modifier :
method.interfaceMethodModifier()) {
+ if (modifier.STATIC() != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Maps a no-arg {@code getX()}/{@code isX()} declaration to its bean
* property name (JavaBeans decapitalize rule, mirroring
@@ -376,18 +435,35 @@ public final class JavaSourceTypeParser {
return new String(chars);
}
- private static String constructorSignature(String simpleName,
JavaParser.FormalParametersContext params) {
+ /**
+ * A method's declared return type as a simple name, or {@code "void"} —
+ * the grammar models a void return as a {@code typeTypeOrVoid} with no
+ * {@code typeType}.
+ */
+ private static String returnTypeOf(JavaParser.TypeTypeOrVoidContext
returnType) {
+ return (returnType == null || returnType.typeType() == null)
+ ? "void" : simplify(returnType.typeType());
+ }
+
+ /**
+ * {@code name(ParamSimple, ParamSimple)} — the shape
+ * {@code ClassMemberIndex} reports constructors and static methods in, so
+ * the source and compiled views render identically. A varargs tail keeps
+ * its {@code ...}.
+ */
+ private static String signatureOf(String name,
JavaParser.FormalParametersContext params) {
List<String> types = new ArrayList<>();
JavaParser.FormalParameterListContext list = params == null ? null :
params.formalParameterList();
if (list != null) {
for (JavaParser.FormalParameterContext fp :
list.formalParameter()) {
- types.add(simplify(fp.typeType()));
+ types.add(simplify(fp.typeType()) +
dimensions(fp.variableDeclaratorId().LBRACK()));
}
- if (list.lastFormalParameter() != null) {
- types.add(simplify(list.lastFormalParameter().typeType()) +
"...");
+ JavaParser.LastFormalParameterContext last =
list.lastFormalParameter();
+ if (last != null) {
+ types.add(simplify(last.typeType()) +
dimensions(last.variableDeclaratorId().LBRACK()) + "...");
}
}
- return simpleName + "(" + String.join(", ", types) + ")";
+ return name + "(" + String.join(", ", types) + ")";
}
private static List<String> simplifyAll(JavaParser.TypeListContext
typeList) {
@@ -422,21 +498,27 @@ public final class JavaSourceTypeParser {
return typeType == null ? null : simplify(typeType.getText());
}
- /** Strips generic type arguments and array brackets, then takes the last
dotted segment. */
+ /**
+ * Strips generic type arguments and the package prefix while keeping array
+ * dimensions — the shape {@link Class#getSimpleName()} reports, so the
+ * source and compiled views render a type identically.
+ */
private static String simplify(String rawType) {
if (rawType == null) {
return null;
}
String t = rawType;
+ StringBuilder dimensions = new StringBuilder();
+ while (t.endsWith("[]")) {
+ t = t.substring(0, t.length() - 2);
+ dimensions.append("[]");
+ }
int generics = t.indexOf('<');
if (generics >= 0) {
t = t.substring(0, generics);
}
- while (t.endsWith("[]")) {
- t = t.substring(0, t.length() - 2);
- }
int dot = t.lastIndexOf('.');
- return dot >= 0 ? t.substring(dot + 1) : t;
+ return (dot >= 0 ? t.substring(dot + 1) : t) + dimensions;
}
/** The raw text between an {@code arguments()} node's parentheses. */
diff --git
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/ClassMemberIndexTest.java
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/ClassMemberIndexTest.java
index dbfa2d6b9db..a40c01978a0 100644
---
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/ClassMemberIndexTest.java
+++
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/ClassMemberIndexTest.java
@@ -152,8 +152,83 @@ class ClassMemberIndexTest {
ClassMemberIndex.empty().close(); // must not throw
}
+ // ── static view (Type.NAME position) ─────────────────────────────────
+
+ private static final String ROUNDING =
"org.drools.completion.fixtures.Rounding";
+
+ @Test
+ void staticFieldsCarryTheirTypes() {
+ List<Field> statics = index.staticFieldsOf(ROUNDING);
+
+ assertThat(statics).extracting(f -> f.name).contains("SCALE", "MODE");
+ assertThat(statics).anySatisfy(f -> {
+ assertThat(f.name).isEqualTo("SCALE");
+ assertThat(f.type).isEqualTo("int");
+ });
+ }
+
+ /** The two views are disjoint: a static is not a fact property, and vice
versa. */
+ @Test
+ void staticAndInstanceViewsDoNotLeakIntoEachOther() {
+ assertThat(index.staticFieldsOf(ROUNDING)).extracting(f -> f.name)
+ .doesNotContain("applied", "label", "getLabel");
+ assertThat(index.membersOf(ROUNDING)).extracting(f -> f.name)
+ .contains("applied", "label")
+ .doesNotContain("SCALE", "MODE");
+ }
+
+ @Test
+ void staticMethodsAreReportedAsSignatures() {
+ List<String> methods = index.staticMethodsOf(ROUNDING);
+
+ assertThat(methods).contains("roundHalfUp(double) : int",
"describe(int, String) : String",
+ "join(String, String...) : String");
+ assertThat(methods).noneMatch(m -> m.startsWith("getLabel"));
+
assertThat(index.constructorsOf(ROUNDING)).containsExactly("Rounding(String...)");
+ }
+
+ /** Enum constants are public static fields, so they belong to the static
view too. */
+ @Test
+ void enumConstantsAppearAsStaticFields() {
+
assertThat(index.staticFieldsOf("org.drools.completion.fixtures.PetKind"))
+ .extracting(f -> f.name).contains("CAT", "DOG");
+ }
+
+ @Test
+ void staticViewOfUnknownTypeIsEmpty() {
+ assertThat(index.staticFieldsOf("does.not.Exist")).isEmpty();
+ assertThat(index.staticMethodsOf("does.not.Exist")).isEmpty();
+
assertThat(ClassMemberIndex.empty().staticFieldsOf(ROUNDING)).isEmpty();
+ }
+
+ @Test
+ void staticViewFallsBackToSourceWhenClassNotLoadable() {
+ ClassMemberIndex idx = new
ClassMemberIndex(getClass().getClassLoader());
+ idx.setSourceFallback(fakeSource());
+
+ assertEquals(List.of("LIMIT"),
+ idx.staticFieldsOf("com.example.Only").stream().map(f ->
f.name).toList());
+ assertEquals(List.of("of(String) : Only"),
idx.staticMethodsOf("com.example.Only"));
+ }
+
+ /** Reading a static's type must not initialize the class. */
+ @Test
+ void staticViewDoesNotRunStaticInitializers() {
+ index.staticFieldsOf("org.drools.completion.fixtures.Pet");
+ index.staticMethodsOf("org.drools.completion.fixtures.Pet");
+ assertThat(InitProbe.petInitialized).isFalse();
+ }
+
private JavaMemberSource fakeSource() {
return new JavaMemberSource() {
+ public List<Field> staticFieldsOf(String fqcn) {
+ return "com.example.Only".equals(fqcn)
+ ? List.of(new Field("LIMIT", "int", null,
Field.Origin.FIELD))
+ : List.of();
+ }
+ public List<String> staticMethodsOf(String fqcn) {
+ return "com.example.Only".equals(fqcn) ? List.of("of(String) :
Only") : List.of();
+ }
public List<Field> membersOf(String fqcn) {
return "com.example.Only".equals(fqcn)
? List.of(new Field("code", "String", null,
Field.Origin.FIELD),
diff --git
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLCompletionHelperTest.java
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLCompletionHelperTest.java
index 2a0016b6adf..35cca67e6f1 100644
---
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLCompletionHelperTest.java
+++
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLCompletionHelperTest.java
@@ -25,6 +25,7 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
@@ -717,6 +718,187 @@ class DRLCompletionHelperTest {
assertThat(result).isNotNull();
}
+ // ── statics after a type name (issue 3966) ───────────────────────────
+
+ /** Caret immediately after {@code marker}, so no column is counted by
hand. */
+ private static Position caretAfter(String text, String marker) {
+ int idx = text.indexOf(marker) + marker.length();
+ int line = (int) text.substring(0, idx).chars().filter(c -> c ==
'\n').count();
+ int lineStart = text.lastIndexOf('\n', idx - 1) + 1;
+ return new Position(line, idx - lineStart);
+ }
+
+ private static final String IMPORTS_ROUNDING =
+ "package demo;\n"
+ + "import org.drools.completion.fixtures.Rounding;\n"
+ + "rule R\nwhen\n";
+
+ /**
+ * Instance properties are the one set that cannot follow a type name, so
+ * after {@code Rounding.} only its statics are legal.
+ */
+ @Test
+ void afterATypeNameOnlyStaticsAreOffered() {
+ String text = IMPORTS_ROUNDING + " Order( total >
Rounding.\nthen\nend\n";
+
+ List<CompletionItem> result = DRLCompletionHelper.getCompletionItems(
+ text, caretAfter(text, "Rounding."), getLanguageClient(),
ClassIndex.empty(),
+ new ClassMemberIndex(getClass().getClassLoader()));
+
+ assertThat(result).extracting(CompletionItem::getLabel)
+ .contains("SCALE", "MODE", "roundHalfUp", "describe")
+ .doesNotContain("applied", "label");
+ assertThat(result)
+ .anySatisfy(i -> {
+ assertThat(i.getLabel()).isEqualTo("SCALE");
+ assertThat(i.getDetail()).isEqualTo("int");
+ })
+ .anySatisfy(i -> {
+ assertThat(i.getLabel()).isEqualTo("describe");
+ assertThat(i.getDetail()).isEqualTo("describe(int, String)
: String");
+ });
+ }
+
+ /** A fully-qualified type name is a type name too, so the same statics
follow it. */
+ @Test
+ void aQualifiedTypeNameOffersItsStatics() {
+ String text = "package demo;\nrule R\nwhen\n"
+ + " Order( total >
org.drools.completion.fixtures.Rounding.\nthen\nend\n";
+ ClassIndex classIndex = ClassIndex.of(
+ Map.of("Rounding",
List.of("org.drools.completion.fixtures.Rounding")));
+
+ List<CompletionItem> result = DRLCompletionHelper.getCompletionItems(
+ text, caretAfter(text, "Rounding."), getLanguageClient(),
classIndex,
+ new ClassMemberIndex(getClass().getClassLoader()));
+
+ assertThat(result).extracting(CompletionItem::getLabel)
+ .contains("SCALE", "MODE", "roundHalfUp", "describe")
+ .doesNotContain("applied", "label");
+ }
+
+ /**
+ * The reported case: {@code Pet} has three bean properties and no statics,
+ * and all three were offered after {@code Pet.} — the one set that cannot
+ * legally follow a type name. Nothing is the right answer here.
+ */
+ @Test
+ void aTypeWithNoStaticsOffersNoInstancePropertiesAfterTheDot() {
+ String text = "package demo;\n"
+ + "import org.drools.completion.fixtures.Pet;\n"
+ + "rule R\nwhen\n Order( x > Pet.\nthen\nend\n";
+
+ List<CompletionItem> result = DRLCompletionHelper.getCompletionItems(
+ text, caretAfter(text, "Pet."), getLanguageClient(),
ClassIndex.empty(),
+ new ClassMemberIndex(getClass().getClassLoader()));
+
+ assertThat(result).extracting(CompletionItem::getLabel)
+ .doesNotContain("name", "friendly", "legs");
+ }
+
+ /** Only the first hop is static: a constant is an ordinary value of its
type. */
+ @Test
+ void theHopAfterAStaticRevertsToInstanceMembers() {
+ String text = IMPORTS_ROUNDING + " Order( total >
Rounding.MODE.\nthen\nend\n";
+
+ List<CompletionItem> result = DRLCompletionHelper.getCompletionItems(
+ text, caretAfter(text, "Rounding.MODE."), getLanguageClient(),
ClassIndex.empty(),
+ new ClassMemberIndex(getClass().getClassLoader()));
+
+ // MODE is a String, so String's bean properties resume.
+ assertThat(result).extracting(CompletionItem::getLabel)
+ .contains("empty", "blank")
+ .doesNotContain("SCALE", "MODE", "roundHalfUp");
+ }
+
+ /** A lower-case head is a value, not a type reference — instance members
stand. */
+ @Test
+ void instancePositionIsUnchangedByTheStaticView() {
+ String text = "package demo;\n"
+ + "declare Pet\n name : String\n legs : int\nend\n"
+ + "rule R\nwhen\n $p : Pet( )\n Order( x >
$p.\nthen\nend\n";
+
+ List<CompletionItem> result = DRLCompletionHelper.getCompletionItems(
+ text, caretAfter(text, "$p."), getLanguageClient(),
ClassIndex.empty(),
+ new ClassMemberIndex(getClass().getClassLoader()));
+
+
assertThat(result).extracting(CompletionItem::getLabel).contains("name",
"legs");
+ }
+
+ /**
+ * A DRL declare has no statics, and its enum constants are already
members,
+ * so the static view must not shadow them.
+ */
+ @Test
+ void declaredEnumConstantsStillFollowTheTypeName() {
+ String text = "package demo;\n"
+ + "declare enum Color\n RED, GREEN, BLUE;\nend\n"
+ + "rule R\nwhen\n Order( c == Color.\nthen\nend\n";
+
+ List<CompletionItem> result = DRLCompletionHelper.getCompletionItems(
+ text, caretAfter(text, "Color."), getLanguageClient(),
ClassIndex.empty(),
+ new ClassMemberIndex(getClass().getClassLoader()));
+
+ assertThat(result).extracting(CompletionItem::getLabel)
+ .contains("RED", "GREEN", "BLUE");
+ }
+
+ /** A declare has no statics, so nothing can legally follow its name. */
+ @Test
+ void aDeclaredTypeOffersNothingAfterItsName() {
+ String text = "package demo;\n"
+ + "declare Pet\n name : String\n legs : int\nend\n"
+ + "rule R\nwhen\n Order( x > Pet.\nthen\nend\n";
+
+ List<CompletionItem> result = DRLCompletionHelper.getCompletionItems(
+ text, caretAfter(text, "Pet."), getLanguageClient(),
ClassIndex.empty(),
+ new ClassMemberIndex(getClass().getClassLoader()));
+
+ assertThat(result).isEmpty();
+ }
+
+ private static final String DECLARED_ENUM_WITH_FIELD =
+ "package demo;\n"
+ + "declare enum Color\n RED(\"r\"), GREEN(\"g\");\n code :
String\nend\n"
+ + "rule R\nwhen\n";
+
+ @Test
+ void aDeclaredEnumOffersOnlyItsConstantsAfterItsName() {
+ String text = DECLARED_ENUM_WITH_FIELD + " Order( c ==
Color.\nthen\nend\n";
+
+ List<CompletionItem> result = DRLCompletionHelper.getCompletionItems(
+ text, caretAfter(text, "Color."), getLanguageClient(),
ClassIndex.empty(),
+ new ClassMemberIndex(getClass().getClassLoader()));
+
+ assertThat(result).extracting(CompletionItem::getLabel)
+ .containsExactlyInAnyOrder("RED", "GREEN");
+ }
+
+ /** A field typed as its own enum is not a constant, so the type name does
not reach it. */
+ @Test
+ void aSelfTypedEnumFieldIsNotOfferedAsAConstant() {
+ String text = "package demo;\n"
+ + "declare enum Color\n RED(\"r\"), GREEN(\"g\");\n code :
String\n next : Color\nend\n"
+ + "rule R\nwhen\n Order( c == Color.\nthen\nend\n";
+
+ List<CompletionItem> result = DRLCompletionHelper.getCompletionItems(
+ text, caretAfter(text, "Color."), getLanguageClient(),
ClassIndex.empty(),
+ new ClassMemberIndex(getClass().getClassLoader()));
+
+ assertThat(result).extracting(CompletionItem::getLabel)
+ .containsExactlyInAnyOrder("RED", "GREEN");
+ }
+
+ @Test
+ void theHopAfterADeclaredEnumConstantRevertsToItsFields() {
+ String text = DECLARED_ENUM_WITH_FIELD + " Order( c ==
Color.RED.\nthen\nend\n";
+
+ List<CompletionItem> result = DRLCompletionHelper.getCompletionItems(
+ text, caretAfter(text, "Color.RED."), getLanguageClient(),
ClassIndex.empty(),
+ new ClassMemberIndex(getClass().getClassLoader()));
+
+
assertThat(result).extracting(CompletionItem::getLabel).contains("code");
+ }
+
private List<String> completionItemStrings(List<CompletionItem> result) {
return result.stream().map(CompletionItem::getInsertText).toList();
}
diff --git
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLDeclaredTypeParserTest.java
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLDeclaredTypeParserTest.java
index 1d1bd04d1f1..f10e8f11c51 100644
---
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLDeclaredTypeParserTest.java
+++
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLDeclaredTypeParserTest.java
@@ -70,6 +70,8 @@ class DRLDeclaredTypeParserTest {
.contains("LOW", "HIGH", "level");
assertThat(severity.fields.get(0).type).isEqualTo("Severity");
assertThat(severity.fields.get(0).args).isEqualTo("1");
+ assertThat(severity.fields).extracting(f -> f.origin)
+ .containsExactly(Field.Origin.ENUM_CONSTANT,
Field.Origin.ENUM_CONSTANT, Field.Origin.FIELD);
}
@Test
diff --git
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLHoverHelperTest.java
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLHoverHelperTest.java
index f4f68478f98..69e2d00e19b 100644
---
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLHoverHelperTest.java
+++
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLHoverHelperTest.java
@@ -54,6 +54,125 @@ class DRLHoverHelperTest {
return hover.getContents().getRight().getValue();
}
+ // ── statics after a type name (issue 3966) ───────────────────────────
+
+ /** Caret one character into {@code needle}'s first occurrence. */
+ private static Position caretIn(String text, String needle) {
+ int idx = text.indexOf(needle) + 1;
+ int line = (int) text.substring(0, idx).chars().filter(c -> c ==
'\n').count();
+ int lineStart = text.lastIndexOf('\n', idx - 1) + 1;
+ return new Position(line, idx - lineStart);
+ }
+
+ private static String usingRounding(String constraint) {
+ return "package demo;\n"
+ + "import org.drools.completion.fixtures.Rounding;\n"
+ + "rule R\n when\n Order( " + constraint + " )\n
then\nend\n";
+ }
+
+ private ClassMemberIndex reflecting() {
+ return new ClassMemberIndex(getClass().getClassLoader());
+ }
+
+ @Test
+ void hoverOnAStaticFieldShowsItsType() {
+ String drl = usingRounding("total > Rounding.SCALE");
+
+ String md = content(DRLHoverHelper.hover(drl, caretIn(drl, "SCALE"),
+ ClassIndex.empty(), reflecting(), null));
+
+ assertThat(md).contains("**SCALE** : `int`");
+ assertThat(md).contains("Static field of `Rounding`");
+ }
+
+ @Test
+ void hoverOnAStaticMethodShowsItsSignature() {
+ String drl = usingRounding("x > Rounding.describe");
+
+ String md = content(DRLHoverHelper.hover(drl, caretIn(drl, "describe"),
+ ClassIndex.empty(), reflecting(), null));
+
+ assertThat(md).contains("describe(int, String) : String");
+ assertThat(md).contains("Static method of `Rounding`");
+ }
+
+ /** Past the first hop a constant is an ordinary value of its own type. */
+ @Test
+ void hoverPastAStaticRevertsToInstanceMembers() {
+ String drl = usingRounding("x > Rounding.MODE.empty");
+
+ String md = content(DRLHoverHelper.hover(drl, caretIn(drl, "empty"),
+ ClassIndex.empty(), reflecting(), null));
+
+ assertThat(md).contains("Field of `String`");
+ assertThat(md).doesNotContain("Static");
+ }
+
+ /**
+ * A fully-qualified type name is still a type name, so the same rule holds
+ * past it — which is what keeps {@code Rounding.SCALE} and
+ * {@code org...Rounding.SCALE} describing the same thing.
+ */
+ @Test
+ void aQualifiedTypeNameAlsoTakesTheStaticView() {
+ String drl = "package demo;\n"
+ + "rule R\n when\n"
+ + " Order( x >
org.drools.completion.fixtures.Rounding.SCALE )\n"
+ + " then\nend\n";
+ ClassIndex classIndex = ClassIndex.of(
+ Map.of("Rounding",
List.of("org.drools.completion.fixtures.Rounding")));
+
+ String md = content(DRLHoverHelper.hover(drl, caretIn(drl, "SCALE"),
+ classIndex, reflecting(), null));
+
+ assertThat(md).contains("**SCALE** : `int`");
+ assertThat(md).contains("Static field of `Rounding`");
+ }
+
+ /** An instance property is not reachable through the type name. */
+ @Test
+ void hoverOnAnInstancePropertyAfterATypeNameIsNotDescribed() {
+ String drl = usingRounding("x > Rounding.applied");
+
+ assertThat(DRLHoverHelper.hover(drl, caretIn(drl, "applied"),
+ ClassIndex.empty(), reflecting(), null)).isNull();
+ }
+
+ /** A declare has no statics, so a field after its name is not reachable
either. */
+ @Test
+ void hoverOnAFieldAfterADeclaredTypeNameIsNotDescribed() {
+ String drl = "package demo;\n"
+ + "declare Person\n name : String\n age : int\nend\n"
+ + "rule R\n when\n Person( age > Person.age )\n
then\nend\n";
+
+ assertThat(DRLHoverHelper.hover(drl, caretIn(drl, ".age"),
+ ClassIndex.empty(), ClassMemberIndex.empty(), null)).isNull();
+ }
+
+ @Test
+ void hoverPastADeclaredEnumConstantRevertsToItsFields() {
+ String drl = "package demo;\n"
+ + "declare enum Color\n RED(\"r\"), GREEN(\"g\");\n code :
String\nend\n"
+ + "rule R\n when\n Widget( c == Color.RED.code )\n
then\nend\n";
+
+ String md = content(DRLHoverHelper.hover(drl, caretIn(drl, ".code"),
+ ClassIndex.empty(), ClassMemberIndex.empty(), null));
+
+ assertThat(md).contains("**code** : `String`");
+ assertThat(md).contains("Field of `Color`");
+ }
+
+ /** A field typed as its own enum is not a constant, so the type name does
not reach it. */
+ @Test
+ void hoverOnASelfTypedEnumFieldAfterTheTypeNameIsNotDescribed() {
+ String drl = "package demo;\n"
+ + "declare enum Color\n RED(\"r\"), GREEN(\"g\");\n code :
String\n next : Color\nend\n"
+ + "rule R\n when\n Widget( c == Color.next )\n
then\nend\n";
+
+ assertThat(DRLHoverHelper.hover(drl, caretIn(drl, ".next"),
+ ClassIndex.empty(), ClassMemberIndex.empty(), null)).isNull();
+ }
+
@Test
void hoverParsesTheCurrentDocumentOnce() {
// Declared-type hover (the path that also reads doc + link targets).
@@ -294,29 +413,45 @@ class DRLHoverHelperTest {
assertThat(md).contains("- `Pet()`");
}
+ private static final String MATH_DRL = """
+ package demo;
+
+ rule R
+ when
+ Math()
+ then
+ end
+ """;
+
+ /**
+ * A type whose whole API is static used to render as a bare header, which
+ * is the least useful thing hover can say about it.
+ */
@Test
- void hoverOnClasspathTypeWithNoMembersOrConstructorsRendersHeaderOnly() {
- // java.lang.Math: only static fields/methods, so membersOf is empty,
- // and its sole constructor is private, so constructorsOf is empty too.
- String drl = """
- package demo;
+ void hoverOnAnAllStaticTypeShowsItsStatics() {
+ Hover hover = DRLHoverHelper.hover(MATH_DRL, new Position(4, 4),
+ ClassIndex.empty(), new
ClassMemberIndex(getClass().getClassLoader()), null);
- rule R
- when
- Math()
- then
- end
- """;
- ClassMemberIndex memberIndex = new
ClassMemberIndex(getClass().getClassLoader());
+ String md = content(hover);
+ assertThat(md).contains("java.lang.Math");
+ assertThat(md).contains("**Constants**").contains("- PI : double");
+ assertThat(md).contains("**Static methods**");
+ // Math's only constructor is private, so that section stays absent.
+
assertThat(md).doesNotContain("**Constructors**").doesNotContain("**Fields**");
+ }
- Hover hover = DRLHoverHelper.hover(drl, new Position(4, 4),
- ClassIndex.empty(), memberIndex, null);
+ /** With nothing resolvable, the header is still worth showing on its own.
*/
+ @Test
+ void hoverOnATypeWithNothingResolvableRendersHeaderOnly() {
+ Hover hover = DRLHoverHelper.hover(MATH_DRL, new Position(4, 4),
+ ClassIndex.empty(), ClassMemberIndex.empty(), null);
String md = content(hover);
assertThat(md).contains("java.lang.Math");
assertThat(md).doesNotContain("**Fields**")
.doesNotContain("**Getters**")
.doesNotContain("**Constructors**")
+ .doesNotContain("**Static methods**")
.doesNotContain("**Constants**");
}
diff --git
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/JavaSourceTypeIndexTest.java
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/JavaSourceTypeIndexTest.java
index 79cae1b1518..c6c5bbdd174 100644
---
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/JavaSourceTypeIndexTest.java
+++
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/JavaSourceTypeIndexTest.java
@@ -27,6 +27,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
+import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -57,6 +58,64 @@ class JavaSourceTypeIndexTest {
assertNull(idx.memberNames("com.example.DoesNotExist"));
}
+ private static Set<String> nameAndType(List<Field> fields) {
+ return fields.stream().map(f -> f.name + ":" +
f.type).collect(Collectors.toSet());
+ }
+
+ /**
+ * The source and compiled views must describe the same type identically,
or
+ * a static offered before a build would change shape after it. Compared
+ * against the reflected fixture rather than a hand-written expectation, so
+ * the two implementations are held to each other.
+ */
+ @Test
+ void sourceAndCompiledStaticViewsAgree(@TempDir Path root) throws
Exception {
+ Path src = root.resolve("src/main/java");
+ // Mirrors the compiled fixture
org.drools.completion.fixtures.Rounding.
+ write(src, "com/example/Rounding.java",
+ "package com.example;\npublic class Rounding {\n"
+ + " public static final int SCALE = 2;\n"
+ + " public static final String MODE = \"HALF_EVEN\";\n"
+ + " public static final String[] MODES = {};\n"
+ + " public int applied;\n"
+ + " public Rounding(String... labels) { }\n"
+ + " public static String join(String separator, String... parts)
{ return null; }\n"
+ + " public static int roundHalfUp(double value) { return 0; }\n"
+ + " public static String describe(int scale, String mode) {
return null; }\n"
+ + " public static int[] scales(String[] modes) { return null; }\n"
+ + " public String getLabel() { return null; }\n}\n");
+
+ JavaSourceTypeIndex source = JavaSourceTypeIndex.build(Set.of(src),
List.of());
+ ClassMemberIndex compiled = new
ClassMemberIndex(getClass().getClassLoader());
+ String fixture = "org.drools.completion.fixtures.Rounding";
+
+ assertEquals(nameAndType(compiled.staticFieldsOf(fixture)),
+ nameAndType(source.staticFieldsOf("com.example.Rounding")));
+ assertEquals(Set.copyOf(compiled.staticMethodsOf(fixture)),
+ Set.copyOf(source.staticMethodsOf("com.example.Rounding")));
+ assertEquals(Set.copyOf(compiled.constructorsOf(fixture)),
+ Set.copyOf(source.constructorsOf("com.example.Rounding")));
+ }
+
+ @Test
+ void sourceAndCompiledInterfaceStaticViewsAgree(@TempDir Path root) throws
Exception {
+ Path src = root.resolve("src/main/java");
+ // Mirrors the compiled fixture org.drools.completion.fixtures.Limits.
+ write(src, "com/example/Limits.java",
+ "package com.example;\npublic interface Limits {\n"
+ + " int MAX = 10;\n"
+ + " static String describe(int value) { return null; }\n}\n");
+
+ JavaSourceTypeIndex source = JavaSourceTypeIndex.build(Set.of(src),
List.of());
+ ClassMemberIndex compiled = new
ClassMemberIndex(getClass().getClassLoader());
+ String fixture = "org.drools.completion.fixtures.Limits";
+
+ assertEquals(nameAndType(compiled.staticFieldsOf(fixture)),
+ nameAndType(source.staticFieldsOf("com.example.Limits")));
+ assertEquals(Set.copyOf(compiled.staticMethodsOf(fixture)),
+ Set.copyOf(source.staticMethodsOf("com.example.Limits")));
+ }
+
/**
* A public static field is not a fact property, so it stays out of the
* member list — but it is still written as Type.NAME, so it has to be in
the
diff --git
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/JavaSourceTypeParserTest.java
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/JavaSourceTypeParserTest.java
index d4da542246d..44f4fd0c4a1 100644
---
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/JavaSourceTypeParserTest.java
+++
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/JavaSourceTypeParserTest.java
@@ -64,6 +64,141 @@ class JavaSourceTypeParserTest {
() -> "nor is a static getter: " + t.members);
}
+ /** The other half of that split: what {@code Type.NAME} can reach. */
+ @Test
+ void staticFieldsAndMethodsAreCapturedWithTypes() {
+ JavaSourceType t = only(
+ "package com.example;\n"
+ + "public class Order {\n"
+ + " public static final String VERSION = \"1\";\n"
+ + " public static final int LIMIT = 10;\n"
+ + " public int id;\n"
+ + " public static Order of(String code, int qty) { return null;
}\n"
+ + " public String getCode() { return \"c\"; }\n"
+ + "}\n");
+
+ assertEquals(List.of("VERSION", "LIMIT"),
+ t.staticFields.stream().map(f -> f.name).toList());
+ assertEquals("String", t.staticFields.get(0).type);
+ assertEquals("int", t.staticFields.get(1).type);
+ assertEquals(List.of("of(String, int) : Order"), t.staticMethods);
+ }
+
+ /**
+ * An interface field is implicitly {@code public static final}, so it is a
+ * constant rather than a fact property — which is how reflection reports
it
+ * once the interface is compiled.
+ */
+ @Test
+ void interfaceConstantsAreStaticsNotMembers() {
+ JavaSourceType t = only(
+ "package com.example;\n"
+ + "public interface Limits {\n"
+ + " int MAX = 10;\n"
+ + " String getName();\n"
+ + "}\n");
+
+ assertEquals(List.of("MAX"), t.staticFields.stream().map(f ->
f.name).toList());
+ assertTrue(member(t, "MAX").isEmpty(), () -> "members=" + t.members);
+ assertTrue(member(t, "name").isPresent(), () -> "members=" +
t.members);
+ }
+
+ /**
+ * A static interface method is implicitly public and reachable as
+ * {@code Type.name()}, which is how reflection reports it; a private one
is
+ * neither, and a static getter is not a property.
+ */
+ @Test
+ void interfaceStaticMethodsAreCapturedAndAreNotMembers() {
+ JavaSourceType t = only(
+ "package com.example;\n"
+ + "public interface Limits {\n"
+ + " int MAX = 10;\n"
+ + " static Limits of(int max) { return null; }\n"
+ + " static String getLabel() { return \"l\"; }\n"
+ + " private static int hidden() { return 0; }\n"
+ + " String getName();\n"
+ + "}\n");
+
+ assertEquals(List.of("of(int) : Limits", "getLabel() : String"),
t.staticMethods);
+ assertTrue(member(t, "label").isEmpty(), () -> "a static getter is not
a member: " + t.members);
+ assertTrue(member(t, "name").isPresent(), () -> "members=" +
t.members);
+ }
+
+ /** A record body may declare statics and extra constructors beside the
canonical one. */
+ @Test
+ void recordStaticsAndConstructorsAreCapturedBesideItsComponents() {
+ JavaSourceType t = only(
+ "package com.example;\n"
+ + "public record Point(int x, int y) {\n"
+ + " public static final Point ORIGIN = new Point(0, 0);\n"
+ + " public static Point of(int x, int y) { return new Point(x,
y); }\n"
+ + " public Point(int both) { this(both, both); }\n"
+ + "}\n");
+
+ assertEquals(List.of("ORIGIN"), t.staticFields.stream().map(f ->
f.name).toList());
+ assertEquals("Point", t.staticFields.get(0).type);
+ assertEquals(List.of("of(int, int) : Point"), t.staticMethods);
+ assertEquals(List.of("Point(int, int)", "Point(int)"), t.constructors);
+ assertTrue(member(t, "x").isPresent(), () -> "members=" + t.members);
+ assertTrue(member(t, "ORIGIN").isEmpty(), () -> "members=" +
t.members);
+ }
+
+ /** Reflection renders an array type as {@code String[]}, so the source
view must as well. */
+ @Test
+ void arrayTypesKeepTheirDimensions() {
+ JavaSourceType t = only(
+ "package com.example;\n"
+ + "public class Order {\n"
+ + " public static final String[] NAMES = {};\n"
+ + " public int[][] grid;\n"
+ + " public static java.util.List<String>[] buckets(int[] sizes,
String[] tags) { return null; }\n"
+ + " public String[] getTags() { return null; }\n"
+ + "}\n");
+
+ assertEquals("String[]", t.staticFields.get(0).type);
+ assertEquals("int[][]", member(t, "grid").orElseThrow().type);
+ assertEquals("String[]", member(t, "tags").orElseThrow().type);
+ assertEquals(List.of("buckets(int[], String[]) : List[]"),
t.staticMethods);
+ }
+
+ /** Java also lets the brackets follow the name; reflection reports the
same array type. */
+ @Test
+ void declaratorLevelBracketsCountTowardTheType() {
+ JavaSourceType t = only(
+ "package com.example;\n"
+ + "public class Order {\n"
+ + " public static String NAMES[] = {};\n"
+ + " public int a[], b[][];\n"
+ + " public static int grid(String rows[])[] { return null; }\n"
+ + " public String getTags()[] { return null; }\n"
+ + "}\n");
+
+ assertEquals("String[]", t.staticFields.get(0).type);
+ assertEquals("int[]", member(t, "a").orElseThrow().type);
+ assertEquals("int[][]", member(t, "b").orElseThrow().type);
+ assertEquals(List.of("grid(String[]) : int[]"), t.staticMethods);
+ assertEquals("String[]", member(t, "tags").orElseThrow().type);
+ }
+
+ @Test
+ void interfaceConstantBracketsCountTowardTheType() {
+ JavaSourceType t = only(
+ "package com.example;\npublic interface Limits {\n int LIMITS[] =
{};\n}\n");
+
+ assertEquals("int[]", t.staticFields.get(0).type);
+ }
+
+ /** A constant is reachable as a member of its enum and as {@code
Enum.NAME}. */
+ @Test
+ void enumConstantsAppearInBothViews() {
+ JavaSourceType t = only(
+ "package com.example;\npublic enum Severity { LOW, HIGH }\n");
+
+ assertEquals(List.of("LOW", "HIGH"), t.staticFields.stream().map(f ->
f.name).toList());
+ assertTrue(member(t, "LOW").isPresent(), () -> "members=" + t.members);
+ }
+
/**
* Reflection reports public methods and public constructors only, so the
* source view must not offer more than the compiled view will: a member
that
diff --git
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/fixtures/Limits.java
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/fixtures/Limits.java
new file mode 100644
index 00000000000..3bce67c72a7
--- /dev/null
+++
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/fixtures/Limits.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.completion.fixtures;
+
+/** Interface reflection fixture carrying the two kinds of static an interface
can declare. */
+public interface Limits {
+
+ int MAX = 10;
+
+ static String describe(int value) {
+ return value + "/" + MAX;
+ }
+}
diff --git
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/fixtures/Rounding.java
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/fixtures/Rounding.java
new file mode 100644
index 00000000000..7fcb1285d1c
--- /dev/null
+++
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/fixtures/Rounding.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.completion.fixtures;
+
+/**
+ * Reflection fixture carrying statics alongside instance members, so the two
+ * views can be shown not to leak into each other.
+ */
+public class Rounding {
+
+ public static final int SCALE = 2;
+ public static final String MODE = "HALF_EVEN";
+ public static final String[] MODES = {"HALF_UP", "HALF_EVEN"};
+
+ public int applied;
+ private String label;
+
+ public Rounding(String... labels) {
+ this.label = String.join(",", labels);
+ }
+
+ public static String join(String separator, String... parts) {
+ return String.join(separator, parts);
+ }
+
+ public static int roundHalfUp(double value) {
+ return (int) Math.round(value);
+ }
+
+ public static String describe(int scale, String mode) {
+ return scale + mode;
+ }
+
+ public static int[] scales(String[] modes) {
+ return new int[modes.length];
+ }
+
+ public String getLabel() {
+ return label;
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]