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]

Reply via email to