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]