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 fff4051ac42 incubator-kie-tools#3713: [drl-vscode-extension] 
unknown-type lint honors same-package sibling imports (#3998)
fff4051ac42 is described below

commit fff4051ac426f4841ecc0eb2d5afffa8f640f72d
Author: Daniel Clark <[email protected]>
AuthorDate: Tue Sep 29 06:56:50 2026 +0200

    incubator-kie-tools#3713: [drl-vscode-extension] unknown-type lint honors 
same-package sibling imports (#3998)
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../org/drools/completion/DRLCompletionHelper.java |  61 ++++--
 .../drools/completion/DRLDeclaredTypeParser.java   | 107 ++++++++++-
 .../java/org/drools/completion/DRLLintHelper.java  |  53 ++++--
 .../drools/completion/DRLWorkspaceTypeIndex.java   | 172 +++++++----------
 .../drools/completion/DRLCompletionHelperTest.java |  80 ++++++++
 .../org/drools/completion/DRLLintHelperTest.java   | 212 +++++++++++++++++++++
 .../completion/DRLWorkspaceTypeIndexTest.java      |  91 +++++++++
 7 files changed, 640 insertions(+), 136 deletions(-)

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 c9bb0c6e712..21eeb3cc8d9 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
@@ -21,6 +21,7 @@ package org.drools.completion;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -501,7 +502,8 @@ public class DRLCompletionHelper {
      * <ol>
      *   <li>Already qualified — returned as-is.</li>
      *   <li>Exact (non-wildcard) import match.</li>
-     *   <li>Wildcard import match verified through the class index.</li>
+     *   <li>Wildcard import match verified through the class index — the named
+     *       package only, and skipped when two wildcards provide the 
name.</li>
      *   <li>Class index match for the simple name (skipped when 
ambiguous).</li>
      *   <li>{@code java.lang.*} — implicitly available in DRL without an 
import,
      *       resolved via the platform class loader.</li>
@@ -512,29 +514,54 @@ public class DRLCompletionHelper {
     static String resolveFqcn(String patternType, String simpleName,
                               DRL10Parser.CompilationUnitContext 
compilationUnit,
                               ClassIndex classIndex) {
+        return resolveFqcn(patternType, simpleName, compilationUnit, 
classIndex, List.of());
+    }
+
+    /**
+     * As {@link #resolveFqcn(String, String, 
DRL10Parser.CompilationUnitContext, ClassIndex)},
+     * but with {@code extraImports} unioned onto the document's own imports 
before
+     * resolution — both the exact and the wildcard branch see the union. Lets 
the
+     * unknown-type lint honor imports declared in same-package sibling files 
(which
+     * Drools merges into one namespace) without re-resolving. The 
four-argument
+     * overload delegates here with an empty collection, so its behavior is 
unchanged.
+     */
+    static String resolveFqcn(String patternType, String simpleName,
+                              DRL10Parser.CompilationUnitContext 
compilationUnit,
+                              ClassIndex classIndex, Collection<String> 
extraImports) {
         if (patternType.indexOf('.') >= 0) {
             return patternType;
         }
-        Set<String> imports = extractImports(compilationUnit);
+        Set<String> imports = new HashSet<>(extractImports(compilationUnit));
+        if (extraImports != null) {
+            imports.addAll(extraImports);
+        }
         // 1. Exact import.
         for (String imported : imports) {
             if (imported.endsWith("." + simpleName)) {
                 return imported;
             }
         }
-        // 2. Wildcard import — verify the package actually provides the type 
via
-        //    the class index.
+        // 2. Wildcard imports: the named package's own types only (JLS 7.5.2),
+        //    confirmed by the class index. A name that two wildcards both
+        //    provide is ambiguous (JLS 6.5.5.1), so it resolves to nothing, as
+        //    step 3 does for the bare class index.
+        Set<String> wildcardMatches = new HashSet<>();
         for (String imported : imports) {
             if (imported.endsWith(".*")) {
-                String pkg = imported.substring(0, imported.length() - 1); // 
keep the dot
-                for (String fqcn : classIndex.getMatching(simpleName)) {
-                    if (fqcn.startsWith(pkg)
-                            && (fqcn.endsWith("." + simpleName) || 
fqcn.equals(simpleName))) {
-                        return fqcn;
-                    }
+                String candidate = imported.substring(0, imported.length() - 
1) + simpleName;
+                if (classIndex.forSimpleName(simpleName).contains(candidate)) {
+                    wildcardMatches.add(candidate);
                 }
             }
         }
+        if (wildcardMatches.size() == 1) {
+            return wildcardMatches.iterator().next();
+        }
+        if (wildcardMatches.size() > 1) {
+            logger.log(Level.FINE, () -> "Ambiguous simple name '" + simpleName
+                    + "' under wildcard imports " + wildcardMatches);
+            return null;
+        }
         // 3. Class index (any package). An unqualified name with two classpath
         //    classes sharing a simple name is ambiguous, so it is skipped.
         Set<String> matches = new HashSet<>();
@@ -626,12 +653,22 @@ public class DRLCompletionHelper {
         return items;
     }
 
+    /**
+     * Non-static type imports as qualified names. A wildcard import keeps its
+     * {@code .*} suffix, which the grammar carries as a separate
+     * {@code (DOT MUL)} outside {@code drlQualifiedName} — reconstructed here 
so
+     * the wildcard branch of {@link #resolveFqcn} sees local wildcards the 
same
+     * way it sees the sibling-file ones from
+     * {@link DRLDeclaredTypeParser#cachedFileInfo}.
+     */
     private static Set<String> 
extractImports(DRL10Parser.CompilationUnitContext compilationUnit) {
         Set<String> imports = new HashSet<>();
         for (DRL10Parser.DrlStatementdefContext stmt : 
compilationUnit.drlStatementdef()) {
             if (stmt.importdef() instanceof 
DRL10Parser.ImportStandardDefContext importDef) {
-                if (importDef.DRL_FUNCTION() == null && importDef.STATIC() == 
null) {
-                    imports.add(importDef.drlQualifiedName().getText());
+                if (importDef.DRL_FUNCTION() == null && importDef.STATIC() == 
null
+                        && importDef.drlQualifiedName() != null) {
+                    String name = importDef.drlQualifiedName().getText();
+                    imports.add(importDef.MUL() != null ? name + ".*" : name);
                 }
             }
         }
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 9f70f69fba1..25509571ef8 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
@@ -48,10 +48,37 @@ public final class DRLDeclaredTypeParser {
     private static final class CachedEntry {
         final long modMillis;
         final List<DeclaredType> types;
+        final String packageName;
+        final List<String> imports;
 
-        CachedEntry(long modMillis, List<DeclaredType> types) {
+        CachedEntry(long modMillis, List<DeclaredType> types, String 
packageName,
+                    List<String> imports) {
             this.modMillis = modMillis;
             this.types = types;
+            this.packageName = packageName;
+            this.imports = imports;
+        }
+    }
+
+    /**
+     * A file's package name, imports, and declared types, all read from a 
single
+     * parse. The package name is the empty string when the file declares none;
+     * imports keep any {@code .*} wildcard suffix so they can drive
+     * class-index-verified wildcard resolution, matching what
+     * {@code DRLCompletionHelper} extracts for the current document. All lists
+     * are unmodifiable.
+     */
+    public static final class FileInfo {
+        static final FileInfo EMPTY = new FileInfo("", List.of(), List.of());
+
+        final String packageName;
+        final List<String> imports;
+        final List<DeclaredType> types;
+
+        FileInfo(String packageName, List<String> imports, List<DeclaredType> 
types) {
+            this.packageName = packageName == null ? "" : packageName;
+            this.imports = imports;
+            this.types = types;
         }
     }
 
@@ -74,23 +101,53 @@ public final class DRLDeclaredTypeParser {
      * an empty list.
      */
     public static List<DeclaredType> parseDeclaredTypesCached(Path file) {
+        return cachedFileInfo(file).types;
+    }
+
+    /**
+     * Returns the package name, imports, and declared types of {@code file},
+     * serving a cached result while the file's modification time is unchanged.
+     * Missing/unreadable files yield {@link FileInfo#EMPTY}. Shares the one
+     * mtime-keyed cache with {@link #parseDeclaredTypesCached}, so a sibling 
is
+     * parsed once for both its declares and its imports.
+     */
+    public static FileInfo cachedFileInfo(Path file) {
         if (file == null || !Files.isRegularFile(file)) {
-            return Collections.emptyList();
+            return FileInfo.EMPTY;
         }
         try {
             Path key = file.toAbsolutePath().normalize();
             long modMillis = Files.getLastModifiedTime(file).toMillis();
             CachedEntry cached = FILE_CACHE.get(key);
             if (cached != null && cached.modMillis == modMillis) {
-                return cached.types;
+                return new FileInfo(cached.packageName, cached.imports, 
cached.types);
             }
-            List<DeclaredType> types =
-                    
Collections.unmodifiableList(parseDeclaredTypes(Files.readString(file)));
-            FILE_CACHE.put(key, new CachedEntry(modMillis, types));
-            return types;
+            FileInfo info = parseFileInfo(Files.readString(file));
+            FILE_CACHE.put(key, new CachedEntry(modMillis, info.types, 
info.packageName, info.imports));
+            return info;
         } catch (Exception e) {
             logger.fine(() -> "Failed to read/parse " + file + ": " + 
e.getMessage());
-            return Collections.emptyList();
+            return FileInfo.EMPTY;
+        }
+    }
+
+    /**
+     * Parses the package name, imports, and declared types from {@code text}
+     * (uncached — for open unsaved buffers). Parser errors are swallowed so a
+     * partial file still yields partial results.
+     */
+    public static FileInfo parseFileInfo(String text) {
+        try {
+            DRL10Parser.CompilationUnitContext cu = 
DRLParsers.silent(text).compilationUnit();
+            if (cu == null) {
+                return FileInfo.EMPTY;
+            }
+            return new FileInfo(extractPackageName(cu),
+                    Collections.unmodifiableList(extractImports(cu)),
+                    
Collections.unmodifiableList(extractFromCompilationUnit(cu)));
+        } catch (Exception e) {
+            logger.fine(() -> "Failed to parse DRL for file info: " + 
e.getMessage());
+            return FileInfo.EMPTY;
         }
     }
 
@@ -170,6 +227,40 @@ public final class DRLDeclaredTypeParser {
         return types;
     }
 
+    /**
+     * The compilation unit's declared package, or the empty string when it
+     * declares none. Grammar: {@code packagedef : PACKAGE 
name=drlQualifiedName SEMI?}.
+     */
+    static String extractPackageName(DRL10Parser.CompilationUnitContext cu) {
+        if (cu == null || cu.packagedef() == null || 
cu.packagedef().drlQualifiedName() == null) {
+            return "";
+        }
+        return cu.packagedef().drlQualifiedName().getText();
+    }
+
+    /**
+     * The compilation unit's standard imports (excluding {@code import 
function}
+     * and {@code import static}), each as its qualified name. A wildcard 
import
+     * keeps its {@code .*} suffix, which the grammar carries as a separate
+     * {@code (DOT MUL)} outside {@code drlQualifiedName} — reconstructed here 
so
+     * wildcard imports read the same way local ones do downstream.
+     */
+    private static List<String> 
extractImports(DRL10Parser.CompilationUnitContext cu) {
+        List<String> imports = new ArrayList<>();
+        for (DRL10Parser.DrlStatementdefContext stmt : cu.drlStatementdef()) {
+            if (stmt.importdef() instanceof 
DRL10Parser.ImportStandardDefContext importDef
+                    && importDef.DRL_FUNCTION() == null && importDef.STATIC() 
== null
+                    && importDef.drlQualifiedName() != null) {
+                String name = importDef.drlQualifiedName().getText();
+                if (importDef.MUL() != null) {
+                    name = name + ".*";
+                }
+                imports.add(name);
+            }
+        }
+        return imports;
+    }
+
     private static DeclaredType 
extractTypeDeclaration(DRL10Parser.TypeDeclarationContext ctx) {
         if (ctx == null || ctx.name == null) {
             return null;
diff --git 
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLLintHelper.java
 
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLLintHelper.java
index ac97bb038f1..c72f0b3dc1c 100644
--- 
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLLintHelper.java
+++ 
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLLintHelper.java
@@ -21,6 +21,7 @@ package org.drools.completion;
 
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -805,9 +806,22 @@ public final class DRLLintHelper {
                 declared.putIfAbsent(dt.name, dt);
             }
         }
-        DRLWorkspaceTypeIndex.forEachSiblingType(documentPath, openFiles, (dt, 
uri) -> {
-            if (dt.name != null) {
-                declared.putIfAbsent(dt.name, dt);
+        // Imports declared in same-package sibling files are in scope here too
+        // (Drools merges files by package), so resolution must see them. 
Computed
+        // once and threaded through all three scan paths. Files without a 
package
+        // declaration all compile into the builder's default package
+        // (drools-compiler's CompositePackageCompilationPhase), so two
+        // package-less files merge as well.
+        String ownPackage = DRLDeclaredTypeParser.extractPackageName(cu);
+        List<String> siblingImports = new ArrayList<>();
+        DRLWorkspaceTypeIndex.forEachSiblingInfo(documentPath, openFiles, 
(info, uri) -> {
+            for (DeclaredType dt : info.types) {
+                if (dt.name != null) {
+                    declared.putIfAbsent(dt.name, dt);
+                }
+            }
+            if (ownPackage.equals(info.packageName)) {
+                siblingImports.addAll(info.imports);
             }
         });
         Set<String> known = declared.keySet();
@@ -816,9 +830,10 @@ public final class DRLLintHelper {
         suggestions.addAll(classIndex.simpleNames());
 
         List<Diagnostic> out = new ArrayList<>();
-        collectPatternTypes(cu, cu, known, suggestions, classIndex, 
classpathResolved, severity, out);
+        collectPatternTypes(cu, cu, known, suggestions, classIndex, 
siblingImports,
+                            classpathResolved, severity, out);
         scanQualifiedRefs(cu, sanitized, declared, suggestions, cu, 
classIndex, memberIndex,
-                          classpathResolved, severity, out);
+                          siblingImports, classpathResolved, severity, out);
 
         Matcher then = THEN_END_BLOCK.matcher(sanitized);
         while (then.find() && out.size() < MAX_UNKNOWN_TYPE_DIAGNOSTICS) {
@@ -827,7 +842,7 @@ public final class DRLLintHelper {
             while (newType.find() && out.size() < 
MAX_UNKNOWN_TYPE_DIAGNOSTICS) {
                 Range range = rangeOf(sanitized, base + newType.start(1), base 
+ newType.end(1));
                 addUnknown(newType.group(1), range, known, suggestions, cu, 
classIndex,
-                           classpathResolved, severity, out);
+                           siblingImports, classpathResolved, severity, out);
             }
         }
         return out;
@@ -836,7 +851,8 @@ public final class DRLLintHelper {
     /** Walks {@code node}, checking each pattern's object type against {@code 
cu}'s resolution. */
     private static void collectPatternTypes(ParseTree node, 
DRL10Parser.CompilationUnitContext cu,
                                             Set<String> known, Set<String> 
suggestions,
-                                            ClassIndex classIndex, boolean 
classpathResolved,
+                                            ClassIndex classIndex, 
Collection<String> extraImports,
+                                            boolean classpathResolved,
                                             DiagnosticSeverity severity, 
List<Diagnostic> out) {
         if (out.size() >= MAX_UNKNOWN_TYPE_DIAGNOSTICS) {
             return;
@@ -850,12 +866,12 @@ public final class DRLLintHelper {
                         new Position(stop.getLine() - 1,
                                      stop.getCharPositionInLine() + 
stop.getText().length()));
                 addUnknown(pattern.objectType.getText(), range, known, 
suggestions, cu, classIndex,
-                           classpathResolved, severity, out);
+                           extraImports, classpathResolved, severity, out);
             }
         }
         for (int i = 0; i < node.getChildCount(); i++) {
-            collectPatternTypes(node.getChild(i), cu, known, suggestions, 
classIndex, classpathResolved,
-                                severity, out);
+            collectPatternTypes(node.getChild(i), cu, known, suggestions, 
classIndex, extraImports,
+                                classpathResolved, severity, out);
         }
     }
 
@@ -869,7 +885,8 @@ public final class DRLLintHelper {
     private static void scanQualifiedRefs(ParseTree node, String sanitized,
                                           Map<String, DeclaredType> declared, 
Set<String> suggestions,
                                           DRL10Parser.CompilationUnitContext 
cu, ClassIndex classIndex,
-                                          ClassMemberIndex memberIndex, 
boolean classpathResolved,
+                                          ClassMemberIndex memberIndex, 
Collection<String> extraImports,
+                                          boolean classpathResolved,
                                           DiagnosticSeverity severity, 
List<Diagnostic> out) {
         if (out.size() >= MAX_UNKNOWN_TYPE_DIAGNOSTICS) {
             return;
@@ -881,14 +898,14 @@ public final class DRLLintHelper {
                 Matcher m = QUALIFIED_REF.matcher(sanitized.substring(start, 
stop + 1));
                 while (m.find() && out.size() < MAX_UNKNOWN_TYPE_DIAGNOSTICS) {
                     checkChain(m.group(1), start + m.start(1), sanitized, 
declared, suggestions,
-                               cu, classIndex, memberIndex, classpathResolved, 
severity, out);
+                               cu, classIndex, memberIndex, extraImports, 
classpathResolved, severity, out);
                 }
             }
             return; // the section's whole text is covered; no nested 
when-sections
         }
         for (int i = 0; i < node.getChildCount(); i++) {
             scanQualifiedRefs(node.getChild(i), sanitized, declared, 
suggestions, cu, classIndex,
-                              memberIndex, classpathResolved, severity, out);
+                              memberIndex, extraImports, classpathResolved, 
severity, out);
         }
     }
 
@@ -903,7 +920,8 @@ public final class DRLLintHelper {
     private static void checkChain(String chain, int chainStart, String 
sanitized,
                                    Map<String, DeclaredType> declared, 
Set<String> suggestions,
                                    DRL10Parser.CompilationUnitContext cu, 
ClassIndex classIndex,
-                                   ClassMemberIndex memberIndex, boolean 
classpathResolved,
+                                   ClassMemberIndex memberIndex, 
Collection<String> extraImports,
+                                   boolean classpathResolved,
                                    DiagnosticSeverity severity, 
List<Diagnostic> out) {
         int dot = chain.indexOf('.');
         String head = chain.substring(0, dot);
@@ -927,7 +945,7 @@ public final class DRLLintHelper {
         }
 
         // Classpath head.
-        String headFqcn = DRLCompletionHelper.resolveFqcn(head, head, cu, 
classIndex);
+        String headFqcn = DRLCompletionHelper.resolveFqcn(head, head, cu, 
classIndex, extraImports);
         if (headFqcn == null) {
             // Can't be sure it's a typo vs an unresolved real classpath type.
             if (classpathResolved) {
@@ -973,7 +991,8 @@ public final class DRLLintHelper {
      */
     private static void addUnknown(String candidate, Range range, Set<String> 
known,
                                    Set<String> suggestions, 
DRL10Parser.CompilationUnitContext cu,
-                                   ClassIndex classIndex, boolean 
classpathResolved,
+                                   ClassIndex classIndex, Collection<String> 
extraImports,
+                                   boolean classpathResolved,
                                    DiagnosticSeverity severity, 
List<Diagnostic> out) {
         if (candidate == null) {
             return;
@@ -984,7 +1003,7 @@ public final class DRLLintHelper {
         }
         String simple = toSimpleTypeName(trimmed);
         if (known.contains(simple)
-                || DRLCompletionHelper.resolveFqcn(trimmed, simple, cu, 
classIndex) != null) {
+                || DRLCompletionHelper.resolveFqcn(trimmed, simple, cu, 
classIndex, extraImports) != null) {
             return;
         }
         if (!classpathResolved) {
diff --git 
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLWorkspaceTypeIndex.java
 
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLWorkspaceTypeIndex.java
index 42a198d32cd..ad670d9f6de 100644
--- 
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLWorkspaceTypeIndex.java
+++ 
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLWorkspaceTypeIndex.java
@@ -21,12 +21,12 @@ package org.drools.completion;
 
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 
 /**
  * Central, layered view of the declared types reachable from a DRL document.
@@ -38,10 +38,10 @@ import java.util.function.BiConsumer;
  * <p>Layers, highest priority first:
  * <ol>
  *   <li>the current document;</li>
- *   <li>open <em>unsaved</em> sibling buffers (same directory) — their editor
- *       content is newer than disk, so it shadows the on-disk version;</li>
- *   <li>on-disk sibling {@code .drl} files from the active
- *       {@link WorkspaceSiblingResolver}.</li>
+ *   <li>open <em>unsaved</em> buffers of the sibling files the active
+ *       {@link WorkspaceSiblingResolver} yields — their editor content is
+ *       newer than disk, so it shadows the on-disk version;</li>
+ *   <li>the remaining sibling files, read from disk.</li>
  * </ol>
  *
  * <p>The earliest layer to provide a given name wins. All on-disk reads go
@@ -139,37 +139,20 @@ public final class DRLWorkspaceTypeIndex {
         if (containsName(currentDocTypes, name)) {
             return DRLDocCommentParser.docFor(text, name);
         }
-        if (documentPath == null) {
-            return null;
-        }
-        Path docNorm = documentPath.toAbsolutePath().normalize();
-        Path dir = docNorm.getParent();
-        Set<Path> shadowed = new HashSet<>();
-        // Layer 2: open unsaved siblings.
-        if (openFiles != null) {
-            for (Map.Entry<Path, String> e : openFiles.entrySet()) {
-                Path p = normalizedSibling(e.getKey(), docNorm, dir);
-                if (p == null) {
-                    continue;
-                }
-                shadowed.add(p);
-                if (declaresType(e.getValue(), name)) {
-                    return DRLDocCommentParser.docFor(e.getValue(), name);
-                }
-            }
-        }
-        // Layer 3: on-disk siblings.
-        for (Path sibling : 
WorkspaceSiblingResolvers.active().resolveSiblings(documentPath)) {
-            if (shadowed.contains(sibling.toAbsolutePath().normalize())) {
-                continue;
-            }
-            for (DeclaredType t : 
DRLDeclaredTypeParser.parseDeclaredTypesCached(sibling)) {
-                if (name.equals(t.name)) {
-                    return 
DRLDocCommentParser.docFor(readFileSilently(sibling), name);
-                }
-            }
-        }
-        return null;
+        String[] declaring = {null};
+        forEachSibling(documentPath, openFiles,
+                (sibling, bufferText) -> {
+                    if (declaring[0] == null && declaresType(bufferText, 
name)) {
+                        declaring[0] = bufferText;
+                    }
+                },
+                sibling -> {
+                    if (declaring[0] == null
+                            && 
containsName(DRLDeclaredTypeParser.parseDeclaredTypesCached(sibling), name)) {
+                        declaring[0] = readFileSilently(sibling);
+                    }
+                });
+        return declaring[0] == null ? null : 
DRLDocCommentParser.docFor(declaring[0], name);
     }
 
     /**
@@ -180,38 +163,28 @@ public final class DRLWorkspaceTypeIndex {
      */
     static void forEachSiblingType(Path documentPath, Map<Path, String> 
openFiles,
                                    BiConsumer<DeclaredType, String> sink) {
-        if (documentPath == null) {
-            return;
-        }
-        Path docNorm = documentPath.toAbsolutePath().normalize();
-        Path dir = docNorm.getParent();
-        Set<Path> shadowed = new HashSet<>();
-
-        // Layer 2: open unsaved siblings (same directory, not the current 
file).
-        if (openFiles != null) {
-            for (Map.Entry<Path, String> e : openFiles.entrySet()) {
-                Path p = normalizedSibling(e.getKey(), docNorm, dir);
-                if (p == null) {
-                    continue;
-                }
-                shadowed.add(p);
-                String uri = p.toUri().toString();
-                for (DeclaredType t : 
DRLDeclaredTypeParser.parseDeclaredTypes(e.getValue())) {
-                    sink.accept(t, uri);
-                }
-            }
-        }
-
-        // Layer 3: on-disk siblings not shadowed by an open buffer.
-        for (Path sibling : 
WorkspaceSiblingResolvers.active().resolveSiblings(documentPath)) {
-            if (shadowed.contains(sibling.toAbsolutePath().normalize())) {
-                continue;
-            }
-            String uri = sibling.toUri().toString();
-            for (DeclaredType t : 
DRLDeclaredTypeParser.parseDeclaredTypesCached(sibling)) {
+        forEachSiblingInfo(documentPath, openFiles, (info, uri) -> {
+            for (DeclaredType t : info.types) {
                 sink.accept(t, uri);
             }
-        }
+        });
+    }
+
+    /**
+     * Visits each sibling once — open unsaved buffers first, then on-disk
+     * siblings not shadowed by a buffer — passing its package, imports and
+     * declared types together with its file URI to {@code sink}, so a consumer
+     * needing more than one of them (the unknown-type lint wants both declares
+     * and same-package imports) parses each sibling a single time. The current
+     * document is <em>not</em> included.
+     */
+    static void forEachSiblingInfo(Path documentPath, Map<Path, String> 
openFiles,
+                                   BiConsumer<DRLDeclaredTypeParser.FileInfo, 
String> sink) {
+        forEachSibling(documentPath, openFiles,
+                (sibling, bufferText) -> sink.accept(
+                        DRLDeclaredTypeParser.parseFileInfo(bufferText), 
sibling.toUri().toString()),
+                sibling -> sink.accept(
+                        DRLDeclaredTypeParser.cachedFileInfo(sibling), 
sibling.toUri().toString()));
     }
 
     /**
@@ -225,50 +198,51 @@ public final class DRLWorkspaceTypeIndex {
      */
     static void forEachSiblingFile(Path documentPath, Map<Path, String> 
openFiles,
                                    BiConsumer<String, String> sink) {
+        forEachSibling(documentPath, openFiles,
+                (sibling, bufferText) -> 
sink.accept(sibling.toUri().toString(), bufferText),
+                sibling -> {
+                    String content = readFileSilently(sibling);
+                    if (content != null) {
+                        sink.accept(sibling.toUri().toString(), content);
+                    }
+                });
+    }
+
+    /**
+     * Visits each sibling the active {@link WorkspaceSiblingResolver} yields 
for
+     * {@code documentPath} exactly once, by normalized absolute path: those 
with
+     * an open buffer go to {@code openBuffer} with their editor text, in
+     * resolver order; the rest then go to {@code onDisk}. Membership is the
+     * resolver's alone — an open buffer outside its grouping is not a sibling.
+     */
+    private static void forEachSibling(Path documentPath, Map<Path, String> 
openFiles,
+                                       BiConsumer<Path, String> openBuffer, 
Consumer<Path> onDisk) {
         if (documentPath == null) {
             return;
         }
-        Path docNorm = documentPath.toAbsolutePath().normalize();
-        Path dir = docNorm.getParent();
-        Set<Path> shadowed = new HashSet<>();
-
-        // Layer 2: open unsaved siblings (same directory, not the current 
file).
+        Map<Path, String> buffers = new HashMap<>();
         if (openFiles != null) {
             for (Map.Entry<Path, String> e : openFiles.entrySet()) {
-                Path p = normalizedSibling(e.getKey(), docNorm, dir);
-                if (p == null) {
-                    continue;
+                if (e.getKey() != null && e.getValue() != null) {
+                    buffers.put(e.getKey().toAbsolutePath().normalize(), 
e.getValue());
                 }
-                shadowed.add(p);
-                sink.accept(p.toUri().toString(), e.getValue());
             }
         }
-
-        // Layer 3: on-disk siblings not shadowed by an open buffer.
+        List<Path> siblings = new ArrayList<>();
         for (Path sibling : 
WorkspaceSiblingResolvers.active().resolveSiblings(documentPath)) {
-            if (shadowed.contains(sibling.toAbsolutePath().normalize())) {
-                continue;
-            }
-            String content = readFileSilently(sibling);
-            if (content != null) {
-                sink.accept(sibling.toUri().toString(), content);
-            }
+            siblings.add(sibling.toAbsolutePath().normalize());
         }
-    }
-
-    /**
-     * Returns the normalized form of {@code candidate} when it is a 
same-directory
-     * sibling of {@code docNorm} (and not the document itself), else {@code 
null}.
-     */
-    private static Path normalizedSibling(Path candidate, Path docNorm, Path 
dir) {
-        if (candidate == null || dir == null) {
-            return null;
+        for (Path sibling : siblings) {
+            String bufferText = buffers.get(sibling);
+            if (bufferText != null) {
+                openBuffer.accept(sibling, bufferText);
+            }
         }
-        Path norm = candidate.toAbsolutePath().normalize();
-        if (norm.equals(docNorm) || !dir.equals(norm.getParent())) {
-            return null;
+        for (Path sibling : siblings) {
+            if (!buffers.containsKey(sibling)) {
+                onDisk.accept(sibling);
+            }
         }
-        return norm;
     }
 
     private static void putType(Map<String, DeclaredType> byName, DeclaredType 
t) {
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 35cca67e6f1..9da83fcb0be 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
@@ -29,6 +29,7 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 
+import org.drools.drl.parser.antlr4.DRL10Parser;
 import org.eclipse.lsp4j.CompletionItem;
 import org.eclipse.lsp4j.CompletionItemKind;
 import org.eclipse.lsp4j.Diagnostic;
@@ -899,6 +900,85 @@ class DRLCompletionHelperTest {
         
assertThat(result).extracting(CompletionItem::getLabel).contains("code");
     }
 
+    /**
+     * A simple name shared by two classpath types is ambiguous on the class
+     * index alone, but a wildcard import picks one package — as it does for 
the
+     * compiler. This works only if the extractor keeps the {@code .*} suffix,
+     * which the grammar carries outside {@code drlQualifiedName}.
+     */
+    @Test
+    void localWildcardImportResolvesAmbiguousSimpleName() {
+        String text = """
+                package org.example;
+
+                import com.acme.model.*;
+                """;
+
+        ClassIndex classIndex = ClassIndex.of(Map.of(
+                "Order", List.of("com.acme.model.Order", "com.other.Order")));
+        DRL10Parser.CompilationUnitContext cu = 
ParsedDrl.of(text).compilationUnit;
+
+        assertThat(DRLCompletionHelper.resolveFqcn("Order", "Order", cu, 
classIndex))
+                .isEqualTo("com.acme.model.Order");
+    }
+
+    /** A wildcard import must not resolve a type its package does not 
provide. */
+    @Test
+    void localWildcardImportDoesNotReachOutsideItsPackage() {
+        String text = """
+                package org.example;
+
+                import com.acme.model.*;
+                """;
+
+        ClassIndex classIndex = ClassIndex.of(Map.of(
+                "Order", List.of("com.other.Order", "com.third.Order")));
+        DRL10Parser.CompilationUnitContext cu = 
ParsedDrl.of(text).compilationUnit;
+
+        assertThat(DRLCompletionHelper.resolveFqcn("Order", "Order", cu, 
classIndex)).isNull();
+    }
+
+    /**
+     * JLS 7.5.2: an on-demand import makes available the classes "declared in
+     * the package", not those of its subpackages. The second Order keeps the
+     * bare-name fallback ambiguous, so only the wildcard branch could answer.
+     */
+    @Test
+    void localWildcardImportDoesNotReachIntoSubpackages() {
+        String text = """
+                package org.example;
+
+                import com.acme.*;
+                """;
+
+        ClassIndex classIndex = ClassIndex.of(Map.of(
+                "Order", List.of("com.acme.model.Order", "com.other.Order")));
+        DRL10Parser.CompilationUnitContext cu = 
ParsedDrl.of(text).compilationUnit;
+
+        assertThat(DRLCompletionHelper.resolveFqcn("Order", "Order", cu, 
classIndex)).isNull();
+    }
+
+    /**
+     * JLS 6.5.5.1: "If multiple type-import-on-demand declarations import 
types
+     * with the same name ... the simple type name is ambiguous, and a
+     * compile-time error occurs." Resolving to either would be a guess.
+     */
+    @Test
+    void twoWildcardImportsProvidingTheSameNameResolveToNothing() {
+        String text = """
+                package org.example;
+
+                import com.acme.*;
+                import com.other.*;
+                """;
+
+        ClassIndex classIndex = ClassIndex.of(Map.of(
+                "Order", List.of("com.acme.Order", "com.other.Order")));
+        DRL10Parser.CompilationUnitContext cu = 
ParsedDrl.of(text).compilationUnit;
+
+        assertThat(DRLCompletionHelper.resolveFqcn("Order", "Order", cu, 
classIndex)).isNull();
+    }
+
     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/DRLLintHelperTest.java
 
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLLintHelperTest.java
index fb9c0fed45b..b98f0f2d519 100644
--- 
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLLintHelperTest.java
+++ 
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLLintHelperTest.java
@@ -19,13 +19,19 @@
 
 package org.drools.completion;
 
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import org.eclipse.lsp4j.Diagnostic;
 import org.eclipse.lsp4j.DiagnosticSeverity;
 import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -69,6 +75,20 @@ class DRLLintHelperTest {
         System.clearProperty("drools.lsp.lint.mvelPropertyAccess");
     }
 
+    // Sibling-import tests pin the active resolver to same-directory grouping 
so
+    // they never depend on an ambient ServiceLoader-discovered provider; the
+    // default is restored afterwards. Tests that pass a null document path are
+    // unaffected either way.
+    @BeforeEach
+    void useSameDirectoryResolver() {
+        
WorkspaceSiblingResolvers.setActive(WorkspaceSiblingResolvers::sameDirectorySiblings);
+    }
+
+    @AfterEach
+    void restoreDefaultResolver() {
+        WorkspaceSiblingResolvers.setActive(null);
+    }
+
     // ── missing 'end' ────────────────────────────────────────────────────
 
     @Test
@@ -799,4 +819,196 @@ class DRLLintHelperTest {
                 + "rule R\n  when\n    Animal( legs == PetKind.CAT.ordinal )\n 
 then\nend\n";
         assertThat(lintUnknownTypes(text)).isEmpty();
     }
+
+    // ── sibling imports (same-package) ───────────────────────────────────
+
+    private static final String USES_ORDER =
+            "package demo;\nrule R\n  when\n    Order( )\n  then\nend\n";
+
+    /** Builds a class index from empty {@code .class} files for {@code 
fqcns}. */
+    private static ClassIndex classIndexOf(Path tempDir, String... fqcns) 
throws IOException {
+        Path classesDir = tempDir.resolve("classes");
+        Files.createDirectories(classesDir);
+        for (String fqcn : fqcns) {
+            Path classFile = classesDir.resolve(fqcn.replace('.', '/') + 
".class");
+            Files.createDirectories(classFile.getParent());
+            Files.createFile(classFile);
+        }
+        return ClassIndex.build(Set.of(classesDir));
+    }
+
+    @Test
+    void siblingImportLegalizesPatternType(@TempDir Path tempDir) throws 
IOException {
+        // A same-package sibling imports Order; the current file uses it as a
+        // pattern type without importing it itself. The exact sibling import
+        // makes it resolvable, so no unknown-type diagnostic fires.
+        Path current = tempDir.resolve("current.drl");
+        Files.writeString(current, USES_ORDER);
+        Files.writeString(tempDir.resolve("sibling.drl"),
+                "package demo;\nimport com.example.model.Order;\n");
+
+        List<Diagnostic> diags = DRLLintHelper.lintUnknownTypes(
+                Files.readString(current), current, Map.of(), 
ClassIndex.empty(), members, true);
+
+        assertThat(diags).isEmpty();
+    }
+
+    @Test
+    void siblingWildcardImportLegalizesThroughClassIndex(@TempDir Path 
tempDir) throws IOException {
+        // A wildcard sibling import (com.example.model.*) legalizes Order only
+        // when the class index confirms that package provides it. Two Order
+        // classes make the bare simple name ambiguous, so only the wildcard's
+        // package can disambiguate it.
+        Path current = tempDir.resolve("current.drl");
+        Files.writeString(current, USES_ORDER);
+        Files.writeString(tempDir.resolve("sibling.drl"),
+                "package demo;\nimport com.example.model.*;\n");
+        ClassIndex classIndex = classIndexOf(tempDir, 
"com.example.model.Order", "com.other.Order");
+
+        List<Diagnostic> diags = DRLLintHelper.lintUnknownTypes(
+                Files.readString(current), current, Map.of(), classIndex, 
members, true);
+
+        assertThat(diags).isEmpty();
+    }
+
+    /**
+     * JLS 7.5.2: an on-demand import makes available the classes "declared in
+     * the package", not those of its subpackages. A sibling's wildcard must 
not
+     * legalize a type that lives one package down.
+     */
+    @Test
+    void siblingWildcardImportDoesNotReachIntoSubpackages(@TempDir Path 
tempDir) throws IOException {
+        Path current = tempDir.resolve("current.drl");
+        Files.writeString(current, USES_ORDER);
+        Files.writeString(tempDir.resolve("sibling.drl"),
+                "package demo;\nimport com.example.*;\n");
+        ClassIndex classIndex = classIndexOf(tempDir, 
"com.example.model.Order", "com.other.Order");
+
+        List<Diagnostic> diags = DRLLintHelper.lintUnknownTypes(
+                Files.readString(current), current, Map.of(), classIndex, 
members, true);
+
+        assertThat(diags)
+                .singleElement()
+                .satisfies(d -> assertThat(d.getMessage()).contains("Unknown 
type 'Order'"));
+    }
+
+    @Test
+    void differentPackageSiblingContributesNothing(@TempDir Path tempDir) 
throws IOException {
+        // The sibling imports Order but declares a different package, so 
Drools
+        // does not merge it with the current file: the import is out of scope
+        // and the unknown-type diagnostic still fires.
+        Path current = tempDir.resolve("current.drl");
+        Files.writeString(current, USES_ORDER);
+        Files.writeString(tempDir.resolve("sibling.drl"),
+                "package other;\nimport com.example.model.Order;\n");
+
+        List<Diagnostic> diags = DRLLintHelper.lintUnknownTypes(
+                Files.readString(current), current, Map.of(), 
ClassIndex.empty(), members, true);
+
+        assertThat(diags)
+                .singleElement()
+                .satisfies(d -> assertThat(d.getMessage()).contains("Unknown 
type 'Order'"));
+    }
+
+    @Test
+    void unsavedSiblingBufferImportCounts(@TempDir Path tempDir) throws 
IOException {
+        // The on-disk sibling lacks the import, but an open unsaved buffer 
for it
+        // adds one. The buffer shadows disk, so the import is honored 
immediately.
+        Path current = tempDir.resolve("current.drl");
+        Files.writeString(current, USES_ORDER);
+        Path sibling = tempDir.resolve("sibling.drl");
+        Files.writeString(sibling, "package demo;\n");
+        Map<Path, String> openFiles =
+                Map.of(sibling, "package demo;\nimport 
com.example.model.Order;\n");
+
+        List<Diagnostic> diags = DRLLintHelper.lintUnknownTypes(
+                Files.readString(current), current, openFiles, 
ClassIndex.empty(), members, true);
+
+        assertThat(diags).isEmpty();
+    }
+
+    @Test
+    void unsavedSiblingBufferShadowsTheOnDiskImport(@TempDir Path tempDir) 
throws IOException {
+        // The discriminating direction: the on-disk sibling HAS the import,
+        // but the open unsaved buffer removed it. If disk were (wrongly) read
+        // alongside the buffer, the import would still count — it must not.
+        Path current = tempDir.resolve("current.drl");
+        Files.writeString(current, USES_ORDER);
+        Path sibling = tempDir.resolve("sibling.drl");
+        Files.writeString(sibling, "package demo;\nimport 
com.example.model.Order;\n");
+        Map<Path, String> openFiles = Map.of(sibling, "package demo;\n");
+
+        List<Diagnostic> diags = DRLLintHelper.lintUnknownTypes(
+                Files.readString(current), current, openFiles, 
ClassIndex.empty(), members, true);
+
+        assertThat(diags).isNotEmpty();
+    }
+
+    @Test
+    void openBufferOutsideTheResolverGroupingContributesNoImport(@TempDir Path 
tempDir) throws IOException {
+        // The same-directory buffer would legalize Order, but the active 
resolver
+        // groups nothing with the current file, so the buffer is out of scope 
too.
+        Path current = tempDir.resolve("current.drl");
+        Files.writeString(current, USES_ORDER);
+        Path sibling = tempDir.resolve("sibling.drl");
+        Files.writeString(sibling, "package demo;\n");
+        Map<Path, String> openFiles =
+                Map.of(sibling, "package demo;\nimport 
com.example.model.Order;\n");
+        WorkspaceSiblingResolvers.setActive(file -> List.of());
+
+        List<Diagnostic> diags = DRLLintHelper.lintUnknownTypes(
+                Files.readString(current), current, openFiles, 
ClassIndex.empty(), members, true);
+
+        assertThat(diags)
+                .singleElement()
+                .satisfies(d -> assertThat(d.getMessage()).contains("Unknown 
type 'Order'"));
+    }
+
+    @Test
+    void siblingImportDoesNotBypassClasspathGating(@TempDir Path tempDir) 
throws IOException {
+        // Same exact sibling import as the positive case, but the classpath 
has
+        // not resolved and the class index is empty. The exact import resolves
+        // Order without the classpath, so no diagnostic fires — and the pass's
+        // gating (never confirming non-declared names as unknown without a
+        // resolved classpath) is left unchanged.
+        Path current = tempDir.resolve("current.drl");
+        Files.writeString(current, USES_ORDER);
+        Files.writeString(tempDir.resolve("sibling.drl"),
+                "package demo;\nimport com.example.model.Order;\n");
+
+        List<Diagnostic> diags = DRLLintHelper.lintUnknownTypes(
+                Files.readString(current), current, Map.of(), 
ClassIndex.empty(), members, false);
+
+        assertThat(diags).isEmpty();
+    }
+
+    @Test
+    void noPackageDocumentIgnoresSiblingsThatDeclareOne(@TempDir Path tempDir) 
throws IOException {
+        // The current document has no package declaration, so a sibling that
+        // declares one is a different package: its imports do not apply and 
the
+        // unknown-type diagnostic fires.
+        Path current = tempDir.resolve("current.drl");
+        Files.writeString(current, "rule R\n  when\n    Order( )\n  
then\nend\n");
+        Files.writeString(tempDir.resolve("sibling.drl"),
+                "package demo;\nimport com.example.model.Order;\n");
+
+        List<Diagnostic> diags = DRLLintHelper.lintUnknownTypes(
+                Files.readString(current), current, Map.of(), 
ClassIndex.empty(), members, true);
+
+        assertThat(diags)
+                .singleElement()
+                .satisfies(d -> assertThat(d.getMessage()).contains("Unknown 
type 'Order'"));
+    }
+
+    @Test
+    void packageLessFilesShareTheDefaultPackage(@TempDir Path tempDir) throws 
IOException {
+        Path current = tempDir.resolve("current.drl");
+        Files.writeString(current, "rule R\n  when\n    Order( )\n  
then\nend\n");
+        Files.writeString(tempDir.resolve("sibling.drl"), "import 
com.example.model.Order;\n");
+
+        List<Diagnostic> diags = DRLLintHelper.lintUnknownTypes(
+                Files.readString(current), current, Map.of(), 
ClassIndex.empty(), members, true);
+
+        assertThat(diags).isEmpty();
+    }
 }
diff --git 
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLWorkspaceTypeIndexTest.java
 
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLWorkspaceTypeIndexTest.java
index f7ba7b20bed..c0eee2224bc 100644
--- 
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLWorkspaceTypeIndexTest.java
+++ 
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DRLWorkspaceTypeIndexTest.java
@@ -21,8 +21,12 @@ package org.drools.completion;
 
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
@@ -30,6 +34,11 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 class DRLWorkspaceTypeIndexTest {
 
+    @AfterEach
+    void restoreDefaultResolver() {
+        WorkspaceSiblingResolvers.setActive(null);
+    }
+
     // ---- buildLinkTargets ----
 
     @Test
@@ -114,6 +123,88 @@ class DRLWorkspaceTypeIndexTest {
                 .isEqualTo("New doc.");
     }
 
+    // ---- resolver-defined membership for open buffers ----
+
+    @Test
+    void openBufferOutsideTheResolverGroupingIsIgnored(@TempDir Path dir) 
throws Exception {
+        Path member = dir.resolve("member.drl");
+        Files.writeString(member, "declare Member\nend\n");
+        Path excluded = dir.resolve("excluded.drl");
+        Files.writeString(excluded, "declare Excluded\nend\n");
+        Path current = dir.resolve("rules.drl");
+        WorkspaceSiblingResolvers.setActive(file -> List.of(member));
+        Map<Path, String> open = Map.of(excluded, "declare Excluded\nend\n");
+
+        Map<String, DeclaredType> index =
+                DRLWorkspaceTypeIndex.build("declare Foo\nend\n", current, 
open);
+
+        assertThat(index).containsKeys("Foo", 
"Member").doesNotContainKey("Excluded");
+    }
+
+    @Test
+    void openBufferOfACrossDirectorySiblingShadowsDisk(@TempDir Path dir) 
throws Exception {
+        Path sibling = 
Files.createDirectories(dir.resolve("other")).resolve("types.drl");
+        Files.writeString(sibling, "declare Address\n  code : String\nend\n");
+        Path current = dir.resolve("rules.drl");
+        WorkspaceSiblingResolvers.setActive(file -> List.of(sibling));
+        Map<Path, String> open = Map.of(sibling,
+                "declare Address\n  code : String\n  zip : String\nend\n");
+
+        Map<String, DeclaredType> index =
+                DRLWorkspaceTypeIndex.build("declare Foo\nend\n", current, 
open);
+
+        assertThat(index.get("Address").fields).extracting(f -> f.name)
+                .containsExactly("code", "zip");
+    }
+
+    @Test
+    void forEachSiblingFileYieldsBufferTextForResolverSiblingsOnly(@TempDir 
Path dir) throws Exception {
+        Path sibling = 
Files.createDirectories(dir.resolve("other")).resolve("types.drl");
+        Files.writeString(sibling, "declare Address\nend\n");
+        Path excluded = dir.resolve("excluded.drl");
+        Files.writeString(excluded, "declare Excluded\nend\n");
+        Path current = dir.resolve("rules.drl");
+        WorkspaceSiblingResolvers.setActive(file -> List.of(sibling));
+        Map<Path, String> open = Map.of(sibling, "declare Address\n  zip : 
String\nend\n",
+                excluded, "declare Excluded\n  x : int\nend\n");
+        Map<String, String> visited = new LinkedHashMap<>();
+
+        DRLWorkspaceTypeIndex.forEachSiblingFile(current, open, visited::put);
+
+        assertThat(visited).containsOnly(
+                Map.entry(sibling.toUri().toString(), "declare Address\n  zip 
: String\nend\n"));
+    }
+
+    // ---- forEachSiblingInfo: one visit per sibling carrying types and 
imports ----
+
+    @Test
+    void forEachSiblingInfoVisitsEachSiblingOnceWithTypesAndImports(@TempDir 
Path dir) throws Exception {
+        Path onDisk = dir.resolve("disk.drl");
+        Files.writeString(onDisk, "package demo;\nimport 
com.example.A;\ndeclare FromDisk\nend\n");
+        Path buffered = dir.resolve("buffer.drl");
+        Files.writeString(buffered, "package old;\n");
+        Path current = dir.resolve("rules.drl");
+        Map<Path, String> open = Map.of(buffered,
+                "package demo;\nimport com.example.B;\ndeclare 
FromBuffer\nend\n");
+        List<String> visitedUris = new ArrayList<>();
+        Map<String, DRLDeclaredTypeParser.FileInfo> visited = new 
LinkedHashMap<>();
+
+        DRLWorkspaceTypeIndex.forEachSiblingInfo(current, open, (info, uri) -> 
{
+            visitedUris.add(uri);
+            visited.put(uri, info);
+        });
+
+        assertThat(visitedUris).containsExactly(buffered.toUri().toString(), 
onDisk.toUri().toString());
+        DRLDeclaredTypeParser.FileInfo fromBuffer = 
visited.get(buffered.toUri().toString());
+        assertThat(fromBuffer.packageName).isEqualTo("demo");
+        assertThat(fromBuffer.imports).containsExactly("com.example.B");
+        assertThat(fromBuffer.types).extracting(t -> 
t.name).containsExactly("FromBuffer");
+        DRLDeclaredTypeParser.FileInfo fromDisk = 
visited.get(onDisk.toUri().toString());
+        assertThat(fromDisk.packageName).isEqualTo("demo");
+        assertThat(fromDisk.imports).containsExactly("com.example.A");
+        assertThat(fromDisk.types).extracting(t -> 
t.name).containsExactly("FromDisk");
+    }
+
     @Test
     void docForFallsBackToCurrentDocument() {
         String drl = "/** The local type. */\ndeclare Foo\nend\n";


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to