You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kie.apache.org by "drccrd (via GitHub)" <gi...@apache.org> on 2026/10/01 15:53:58 UTC

[PR] kie-tools#4027: compile diagnostics from the project's own Drools engine, on save [incubator-kie-tools]

drccrd opened a new pull request, #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032

   Closes: #4027 
   
   Saving a DRL file compiles its file group with the Drools engine on the project's own classpath and shows the compiler's errors in the editor.
   
   ### Behaviour
   
   - Trigger: `didSave`, skipped while the file has syntax errors; files that do not parse are left out of the compile wherever they sit. Scope: the file's group through the existing `WorkspaceSiblingResolver`; `DRL: Rebuild Workspace` (`drools/rebuildWorkspace`) compiles every DRL file the grouping layer knows (the client-enumerated workspace files, so `files.exclude` applies) and reports a start message and a summary that names the files with errors, also when it was skipped. One compile at a time; a save during a compile queues one more and the latest wins, a queued rebuild keeps its full scope, and a result whose buffer changed meanwhile is discarded.
   - Results are cached per file of the group and merged into the pull answer after the parser and lint diagnostics; the first edit of a file drops its result, so do a group change and `didClose`. Messages carry `source: drools`, the engine's line and column where present, and recovered ranges otherwise (quoted token, analysed expression, rule header). Follow-on messages that only restate an unresolved type are dropped when a root error is present.
   - A workspace without a usable engine gets no compile diagnostics and nothing else changes: the server needs `drools-compiler` and `drools-mvel` on the project classpath, the classic build's own requirement; `kie-api` alone, or `drools-engine` without `drools-mvel`, counts as no engine and is logged once. The compile also needs the project's own classes: a workspace with Java sources but no build output skips the compile and says so once, and a class still missing from the build is named in one warning on the saved file instead of the engine's generic "Rules compilation failed or interrupted". `drools.lsp.compile.onSave` (default on) turns the save trigger off; the command still works. It applies without a restart, pulled with the formatter options through `workspace/configuration`.
   - The server now advertises `TextDocumentSyncOptions` with save notifications instead of the bare `Full` kind, and the engine follows the classpath: it is rebuilt when the class index is, picking up `target/classes` directories created after startup.
   
   ### Notes for review
   
   - The server gains no engine dependency. A new module `drools-lsp-engine-bridge` is compiled against `kie-api` at provided scope and packaged into the server jar; at runtime its classes are defined inside a class loader whose parent is a `URLClassLoader` over the project's resolved classpath with the platform loader as parent. The server's own loader is deliberately not in that chain: it carries a `drools-drl-parser` and an ANTLR runtime for the editor features, and the project's must win inside the engine. Each build runs on a fresh thread so MVEL's thread-locals cannot pin a replaced loader; the previous loader is closed after the build in flight finishes; an engine that turns out unavailable closes its loader at once. The server jar stays at 7.8 MB.
   - Diagnostics come from the engine version the project uses; two machines with two engine versions may report differently, by design.
   - `WorkspaceSiblingResolver` gains a default `workspaceDrlFiles()` so the rebuild compiles the grouping layer's file set rather than walking the disk; the shipped resolver answers from the scan it already holds.
   - Tests run against the engine kie-tools pins for its own build, in test scope only: `drools-compiler`, `drools-mvel` and `drools-io`, the last named explicitly because `drools-completion` excludes it from its parser dependency and Maven keeps that pruned path for the test tree.
   
   ### Follow-ups, not attempted
   
   - The executable-model build: a project on `drools-engine` without `drools-mvel` gets no compile diagnostics today; `buildAll(ExecutableModelProject.class)` from the bridge would give those projects the compiler they use. Left out because it changes which compiler's messages users see.
   - `workspace/diagnostic` support, so a rebuild's errors in files that are not open reach the Problems panel instead of only the summary.
   - The engine loader is rebuilt whenever the class index is (after a Maven build or a `.java` edit), so the first save after such a change pays the engine warm-up again; a finer trigger is possible if that shows in practice.
   - The same typo can be reported by the unknown-type lint and by the engine; suppressing the engine's type-resolution message when a lint diagnostic covers the same range is a small follow-up.
   - A worker JVM instead of the in-process loader, if memory becomes a problem on large projects; the bridge serves both.
   
   Co-authored by [Claude Code](https://claude.com/claude-code)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "drccrd (via GitHub)" <gi...@apache.org>.
drccrd commented on PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#issuecomment-5954757178

   @tkobayas 
   1: added a configurable timeout (default 2 minutes)
   2: fallback added in `fileFor` as suggested
   3: I don't think this is actually an issue: setEngine closes only the previously installed engine, never the current one, so with an empty classpath the engine built over the last good classpath stays installed with an open loader. That is intended during a transient Maven failure. setEngine(null) would also throw, since setEngine calls available() and close() on its argument
   4: tokens in comments (masked) are now skipped
   5 and 6: both addressed in [8abe2b9](https://github.com/apache/incubator-kie-tools/pull/4032/commits/8abe2b901932cd197f7ec15a9a2a0f97749900a4)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "tkobayas (via GitHub)" <gi...@apache.org>.
tkobayas commented on code in PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#discussion_r4180044595


##########
packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/ProjectEngine.java:
##########
@@ -0,0 +1,224 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.lsp.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * The project's own Drools engine, driven through the bridge class.
+ *
+ * <p>The project loader sees the project's classpath and the platform classes
+ * only. The server's loader is deliberately not its parent: the server jar
+ * carries a DRL parser and an ANTLR runtime of its own, and the project's
+ * versions must win inside the engine. The bridge class is defined from this
+ * server's own jar into a child of the project loader, so its references to
+ * kie-api resolve against the project.
+ */
+final class ProjectEngine implements Engine {
+
+    static final String BRIDGE_CLASS = "org.drools.lsp.bridge.EngineBridge";
+    static final String COMPILER_CLASS = "org.drools.compiler.kie.builder.impl.KieServicesImpl";
+    static final String MVEL_CLASS = "org.drools.mvel.MVELConstraint";
+    private static final String BRIDGE_PACKAGE_PREFIX = "org.drools.lsp.bridge.";
+    private static final Logger logger = Logger.getLogger(ProjectEngine.class.getName());
+
+    private final Set<Path> entries;
+    private final URLClassLoader projectLoader;
+    private final Class<?> bridge;
+    private final Method build;
+    private volatile boolean closed;
+
+    private ProjectEngine(Set<Path> entries, URLClassLoader projectLoader, Class<?> bridge, Method build) {
+        this.entries = entries;
+        this.projectLoader = projectLoader;
+        this.bridge = bridge;
+        this.build = build;
+    }
+
+    static ProjectEngine over(Collection<Path> classpathEntries) {
+        Set<Path> entries = new LinkedHashSet<>(classpathEntries);
+        List<URL> urls = new ArrayList<>(entries.size());
+        for (Path entry : entries) {
+            try {
+                urls.add(entry.toUri().toURL());
+            } catch (MalformedURLException e) {
+                logger.fine(() -> "Skipping classpath entry " + entry + ": " + e.getMessage());
+            }
+        }
+        URLClassLoader projectLoader = new URLClassLoader("drools-lsp-project",
+                urls.toArray(new URL[0]), ClassLoader.getPlatformClassLoader());
+        Class<?> bridge = null;
+        Method build = null;
+        if (hasCompiler(projectLoader)) {
+            try {
+                bridge = Class.forName(BRIDGE_CLASS, true, new BridgeClassLoader(projectLoader));
+                build = bridge.getMethod("build", Map.class);
+            } catch (ReflectiveOperationException | LinkageError e) {
+                logger.log(Level.WARNING, "A Drools engine is on the project classpath but the compile bridge did not link", e);
+            }
+        }
+        ProjectEngine engine = new ProjectEngine(entries, projectLoader, bridge, build);
+        if (build == null) {
+            // Permanently unavailable: release the loader's open jar handles now
+            // rather than holding them until the next classpath change replaces it.
+            engine.close();
+        }
+        return engine;
+    }
+
+    Set<Path> entries() {
+        return entries;
+    }
+
+    @Override
+    public boolean available() {
+        return build != null && !closed;
+    }
+
+    @Override
+    public List<Map<String, Object>> build(Map<String, String> drlByPath, Duration timeout) throws Exception {
+        if (!available()) {
+            throw new IllegalStateException("No Drools engine available");
+        }
+        AtomicReference<Object> outcome = new AtomicReference<>();
+        Thread worker = new Thread(() -> {
+            Thread.currentThread().setContextClassLoader(bridge.getClassLoader());
+            try {
+                outcome.set(build.invoke(null, drlByPath));
+            } catch (Throwable t) {
+                outcome.set(t);
+            }
+        }, "drools-lsp-engine-build");
+        worker.setDaemon(true);
+        worker.start();
+        worker.join(Math.max(1, timeout.toMillis()));
+        if (worker.isAlive()) {
+            worker.interrupt();
+            throw new TimeoutException("The Drools engine did not finish the build within " + timeout.toSeconds()
+                    + " seconds; raise drools.lsp.compile.timeoutSeconds if the group needs longer");

Review Comment:
   The copilot's point may not be a real risk. Per Claude: 
   
   The buildExecutor is a single-thread executor, and build() runs inside its submitted task. When the timeout fires and TimeoutException is thrown, the task exits (caught by the catch (Throwable t) in 
   submit()), and only then does the executor pick up the next queued item. setEngine()'s previous::close is also submitted to the same executor, so it cannot race with a build in flight. The one-at-a-time 
   guarantee is maintained by the executor's queue, not by the join() call.
   
   The remaining concern — an interrupted daemon thread that ignores the interrupt — is real but self-limiting: projectLoader.close() invalidates the loader's open JAR handles, so the orphaned thread will hit 
   an IOException on its next class load and stop. The result is discarded since build() already returned with TimeoutException.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#discussion_r4166819955


##########
packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/ProjectEngine.java:
##########
@@ -0,0 +1,224 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.lsp.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * The project's own Drools engine, driven through the bridge class.
+ *
+ * <p>The project loader sees the project's classpath and the platform classes
+ * only. The server's loader is deliberately not its parent: the server jar
+ * carries a DRL parser and an ANTLR runtime of its own, and the project's
+ * versions must win inside the engine. The bridge class is defined from this
+ * server's own jar into a child of the project loader, so its references to
+ * kie-api resolve against the project.
+ */
+final class ProjectEngine implements Engine {
+
+    static final String BRIDGE_CLASS = "org.drools.lsp.bridge.EngineBridge";
+    static final String COMPILER_CLASS = "org.drools.compiler.kie.builder.impl.KieServicesImpl";
+    static final String MVEL_CLASS = "org.drools.mvel.MVELConstraint";
+    private static final String BRIDGE_PACKAGE_PREFIX = "org.drools.lsp.bridge.";
+    private static final Logger logger = Logger.getLogger(ProjectEngine.class.getName());
+
+    private final Set<Path> entries;
+    private final URLClassLoader projectLoader;
+    private final Class<?> bridge;
+    private final Method build;
+    private volatile boolean closed;
+
+    private ProjectEngine(Set<Path> entries, URLClassLoader projectLoader, Class<?> bridge, Method build) {
+        this.entries = entries;
+        this.projectLoader = projectLoader;
+        this.bridge = bridge;
+        this.build = build;
+    }
+
+    static ProjectEngine over(Collection<Path> classpathEntries) {
+        Set<Path> entries = new LinkedHashSet<>(classpathEntries);
+        List<URL> urls = new ArrayList<>(entries.size());
+        for (Path entry : entries) {
+            try {
+                urls.add(entry.toUri().toURL());
+            } catch (MalformedURLException e) {
+                logger.fine(() -> "Skipping classpath entry " + entry + ": " + e.getMessage());
+            }
+        }
+        URLClassLoader projectLoader = new URLClassLoader("drools-lsp-project",
+                urls.toArray(new URL[0]), ClassLoader.getPlatformClassLoader());
+        Class<?> bridge = null;
+        Method build = null;
+        if (hasCompiler(projectLoader)) {
+            try {
+                bridge = Class.forName(BRIDGE_CLASS, true, new BridgeClassLoader(projectLoader));
+                build = bridge.getMethod("build", Map.class);
+            } catch (ReflectiveOperationException | LinkageError e) {
+                logger.log(Level.WARNING, "A Drools engine is on the project classpath but the compile bridge did not link", e);
+            }
+        }
+        ProjectEngine engine = new ProjectEngine(entries, projectLoader, bridge, build);
+        if (build == null) {
+            // Permanently unavailable: release the loader's open jar handles now
+            // rather than holding them until the next classpath change replaces it.
+            engine.close();
+        }
+        return engine;
+    }
+
+    Set<Path> entries() {
+        return entries;
+    }
+
+    @Override
+    public boolean available() {
+        return build != null && !closed;
+    }
+
+    @Override
+    public List<Map<String, Object>> build(Map<String, String> drlByPath, Duration timeout) throws Exception {
+        if (!available()) {
+            throw new IllegalStateException("No Drools engine available");
+        }
+        AtomicReference<Object> outcome = new AtomicReference<>();
+        Thread worker = new Thread(() -> {
+            Thread.currentThread().setContextClassLoader(bridge.getClassLoader());
+            try {
+                outcome.set(build.invoke(null, drlByPath));
+            } catch (Throwable t) {
+                outcome.set(t);
+            }
+        }, "drools-lsp-engine-build");
+        worker.setDaemon(true);
+        worker.start();
+        worker.join(Math.max(1, timeout.toMillis()));
+        if (worker.isAlive()) {
+            worker.interrupt();
+            throw new TimeoutException("The Drools engine did not finish the build within " + timeout.toSeconds()
+                    + " seconds; raise drools.lsp.compile.timeoutSeconds if the group needs longer");

Review Comment:
   Interrupting the worker does not guarantee that the Drools build has stopped. If it ignores or delays interruption, this method returns while the daemon still uses the project loader; the compile gate then permits another build, and an engine replacement can close that loader underneath the first one. This breaks the one-build-at-a-time guarantee and can retain replaced loaders indefinitely. Keep the gate/loader owned until termination is confirmed, or run timed builds behind an isolation boundary that can actually be terminated.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "tkobayas (via GitHub)" <gi...@apache.org>.
tkobayas commented on PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#issuecomment-5986383679

   Thanks!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "tkobayas (via GitHub)" <gi...@apache.org>.
tkobayas commented on PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#issuecomment-6186203929

   @drccrd Please check the blog draft : https://github.com/apache/incubator-kie-website/pull/63 Thanks!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "drccrd (via GitHub)" <gi...@apache.org>.
drccrd commented on code in PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#discussion_r4181559595


##########
packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/ProjectEngine.java:
##########
@@ -0,0 +1,224 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.lsp.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * The project's own Drools engine, driven through the bridge class.
+ *
+ * <p>The project loader sees the project's classpath and the platform classes
+ * only. The server's loader is deliberately not its parent: the server jar
+ * carries a DRL parser and an ANTLR runtime of its own, and the project's
+ * versions must win inside the engine. The bridge class is defined from this
+ * server's own jar into a child of the project loader, so its references to
+ * kie-api resolve against the project.
+ */
+final class ProjectEngine implements Engine {
+
+    static final String BRIDGE_CLASS = "org.drools.lsp.bridge.EngineBridge";
+    static final String COMPILER_CLASS = "org.drools.compiler.kie.builder.impl.KieServicesImpl";
+    static final String MVEL_CLASS = "org.drools.mvel.MVELConstraint";
+    private static final String BRIDGE_PACKAGE_PREFIX = "org.drools.lsp.bridge.";
+    private static final Logger logger = Logger.getLogger(ProjectEngine.class.getName());
+
+    private final Set<Path> entries;
+    private final URLClassLoader projectLoader;
+    private final Class<?> bridge;
+    private final Method build;
+    private volatile boolean closed;
+
+    private ProjectEngine(Set<Path> entries, URLClassLoader projectLoader, Class<?> bridge, Method build) {
+        this.entries = entries;
+        this.projectLoader = projectLoader;
+        this.bridge = bridge;
+        this.build = build;
+    }
+
+    static ProjectEngine over(Collection<Path> classpathEntries) {
+        Set<Path> entries = new LinkedHashSet<>(classpathEntries);
+        List<URL> urls = new ArrayList<>(entries.size());
+        for (Path entry : entries) {
+            try {
+                urls.add(entry.toUri().toURL());
+            } catch (MalformedURLException e) {
+                logger.fine(() -> "Skipping classpath entry " + entry + ": " + e.getMessage());
+            }
+        }
+        URLClassLoader projectLoader = new URLClassLoader("drools-lsp-project",
+                urls.toArray(new URL[0]), ClassLoader.getPlatformClassLoader());
+        Class<?> bridge = null;
+        Method build = null;
+        if (hasCompiler(projectLoader)) {
+            try {
+                bridge = Class.forName(BRIDGE_CLASS, true, new BridgeClassLoader(projectLoader));
+                build = bridge.getMethod("build", Map.class);
+            } catch (ReflectiveOperationException | LinkageError e) {
+                logger.log(Level.WARNING, "A Drools engine is on the project classpath but the compile bridge did not link", e);
+            }
+        }
+        ProjectEngine engine = new ProjectEngine(entries, projectLoader, bridge, build);
+        if (build == null) {
+            // Permanently unavailable: release the loader's open jar handles now
+            // rather than holding them until the next classpath change replaces it.
+            engine.close();
+        }
+        return engine;
+    }
+
+    Set<Path> entries() {
+        return entries;
+    }
+
+    @Override
+    public boolean available() {
+        return build != null && !closed;
+    }
+
+    @Override
+    public List<Map<String, Object>> build(Map<String, String> drlByPath, Duration timeout) throws Exception {
+        if (!available()) {
+            throw new IllegalStateException("No Drools engine available");
+        }
+        AtomicReference<Object> outcome = new AtomicReference<>();
+        Thread worker = new Thread(() -> {
+            Thread.currentThread().setContextClassLoader(bridge.getClassLoader());
+            try {
+                outcome.set(build.invoke(null, drlByPath));
+            } catch (Throwable t) {
+                outcome.set(t);
+            }
+        }, "drools-lsp-engine-build");
+        worker.setDaemon(true);
+        worker.start();
+        worker.join(Math.max(1, timeout.toMillis()));
+        if (worker.isAlive()) {
+            worker.interrupt();
+            throw new TimeoutException("The Drools engine did not finish the build within " + timeout.toSeconds()
+                    + " seconds; raise drools.lsp.compile.timeoutSeconds if the group needs longer");

Review Comment:
   @tkobayas thanks I agree, pushed one more commit though for the "previously missed" point from the last copilot review.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "tkobayas (via GitHub)" <gi...@apache.org>.
tkobayas commented on PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#issuecomment-6006784906

   @yesamer @tiagobento @thiagoelg @jomarko This PR requires one more approval. Can someone have a look?


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "yesamer (via GitHub)" <gi...@apache.org>.
yesamer commented on PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#issuecomment-6140039751

   @tkobayas Yes :)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "drccrd (via GitHub)" <gi...@apache.org>.
drccrd commented on code in PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#discussion_r4166656958


##########
packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/CompileDiagnostics.java:
##########
@@ -0,0 +1,645 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.lsp.server;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import org.drools.completion.DRLDiagnosticHelper;
+import org.drools.completion.WorkspaceScan;
+import org.drools.completion.WorkspaceSiblingResolvers;
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.DiagnosticSeverity;
+import org.eclipse.lsp4j.MessageType;
+import org.eclipse.lsp4j.Range;
+
+/**
+ * Compiles a saved DRL file together with its group through the project's own
+ * engine and keeps the engine's messages per file until the file is edited,
+ * saved again or regrouped. One compile runs at a time; a save during a compile
+ * queues one more and the latest wins.
+ */
+final class CompileDiagnostics {
+
+    static final String SOURCE = "drools";
+    static final String KFS_PREFIX = "src/main/resources/";
+    static final int MAX_DIAGNOSTICS_PER_FILE = 500;
+    static final String NOT_BUILT =
+            "Compile diagnostics need the project's compiled classes. Build the project, then save again.";
+
+    private static final Pattern NOISY_CASCADE = Pattern.compile(
+            "(?i)(unknown\\s*type|cannot\\s*resolve\\s*type|unresolved\\s*type|unable\\s*to\\s*resolve\\s*type|class\\s*not\\s*found)");
+    private static final Logger logger = Logger.getLogger(CompileDiagnostics.class.getName());
+
+    private final DroolsLspServer server;
+    private final Function<Path, String> openText;
+    private final Map<Path, List<Diagnostic>> cached = new ConcurrentHashMap<>();
+    private final AtomicBoolean buildInFlight = new AtomicBoolean(false);
+    private final AtomicReference<PendingBuild> pending = new AtomicReference<>();
+    private final ExecutorService gateExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile-gate"));
+    private final ExecutorService buildExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile"));
+    private volatile boolean onSave = true;
+    private final AtomicReference<Engine> engine = new AtomicReference<>();
+    private final AtomicBoolean notBuiltNoticeShown = new AtomicBoolean(false);
+
+    CompileDiagnostics(DroolsLspServer server, Function<Path, String> openText) {
+        this.server = server;
+        this.openText = openText;
+    }
+
+    boolean onSave() {
+        return onSave;
+    }
+
+    void setOnSave(boolean onSave) {
+        this.onSave = onSave;
+    }
+
+    Engine engine() {
+        return engine.get();
+    }
+
+    /** Installs the engine; the previous one is closed once the build in flight, if any, has finished. */
+    synchronized void setEngine(Engine next) {
+        if (buildExecutor.isShutdown()) {
+            next.close();
+            return;
+        }
+        Engine previous = engine.getAndSet(next);
+        if (previous == null || previous.available() != next.available()) {
+            logger.info(next.available()
+                    ? "Drools engine found on the project classpath; compile diagnostics are on"
+                    : "No usable Drools engine on the project classpath (needs drools-compiler and drools-mvel); "
+                            + "compile diagnostics are off");
+        }
+        if (previous != null && previous != next) {
+            buildExecutor.submit(previous::close);
+        }
+    }
+
+    /** Releases the engine and stops both executors; the close runs after any build in flight. */
+    synchronized void shutdown() {
+        Engine current = engine.getAndSet(null);
+        if (current != null) {
+            buildExecutor.submit(current::close);
+        }
+        gateExecutor.shutdown();
+        buildExecutor.shutdown();
+    }
+
+    List<Diagnostic> cachedFor(Path path) {
+        return path == null ? List.of() : cached.getOrDefault(normalize(path), List.of());
+    }
+
+    void invalidate(Path path) {
+        if (path != null) {
+            cached.remove(normalize(path));
+        }
+    }
+
+    void invalidateAll() {
+        cached.clear();
+    }
+
+    void onDidSave(Path path, String text) {
+        if (!onSave || path == null || text == null) {
+            return;
+        }
+        trigger(path, text, false);
+    }
+
+    void rebuildWorkspace(Path path) {
+        if (path == null) {
+            return;
+        }
+        String text = openText.apply(normalize(path));
+        if (text == null) {
+            text = readQuietly(path);
+        }
+        if (text == null) {
+            server.showMessage(MessageType.Warning, "Workspace compile skipped: cannot read " + path);
+            return;
+        }
+        trigger(path, text, true);
+    }
+
+    private void trigger(Path path, String text, boolean fullScope) {
+        gateExecutor.submit(() -> triggerNow(path, text, fullScope));
+    }
+
+    void triggerNow(Path path, String text, boolean fullScope) {
+        Engine current = engine.get();
+        if (current == null || !current.available()) {
+            logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: no Drools engine on the project classpath");
+            }
+            return;
+        }
+        if (server.projectClassesMissing()) {
+            if (notBuiltNoticeShown.compareAndSet(false, true)) {
+                logger.info(NOT_BUILT);
+                server.showMessage(MessageType.Info, NOT_BUILT);
+            }
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: the project has not been built");
+            }
+            return;
+        }
+        notBuiltNoticeShown.set(false);
+        if (!fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.info("Compile skipped, " + path + " has syntax errors");
+            return;
+        }
+        if (!buildInFlight.compareAndSet(false, true)) {
+            pending.accumulateAndGet(new PendingBuild(path, text, fullScope),
+                    (old, next) -> old != null && old.fullScope && !next.fullScope
+                            ? new PendingBuild(next.path, next.text, true)
+                            : next);
+            drainPendingIfUnclaimed();
+            return;
+        }
+        submit(path, text, fullScope);
+    }
+
+    private void drainPendingIfUnclaimed() {
+        while (pending.get() != null) {
+            if (!buildInFlight.compareAndSet(false, true)) {
+                return;
+            }
+            PendingBuild queued = pending.getAndSet(null);
+            if (queued != null) {
+                submit(queued.path, queued.text, queued.fullScope);
+                return;
+            }
+            buildInFlight.set(false);
+        }
+    }
+
+    private void submit(Path path, String text, boolean fullScope) {
+        buildExecutor.submit(() -> {
+            Engine target = engine.get();
+            boolean updated = false;
+            boolean ran = false;
+            boolean noEngine = false;
+            int errors = 0;
+            int warnings = 0;
+            Compilation result = null;
+            try {
+                if (target == null || !target.available()) {
+                    noEngine = true;
+                    logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+                    return;
+                }
+                if (fullScope) {
+                    server.showMessage(MessageType.Info, "Compiling every DRL file in the workspace");
+                }
+                result = compile(normalize(path), text, fullScope, target);
+                updated = commit(result);
+                errors = result.count(DiagnosticSeverity.Error);
+                warnings = result.count(DiagnosticSeverity.Warning);
+                ran = true;
+            } catch (Throwable t) {
+                logger.log(Level.WARNING, "Drools compile failed for " + path, t);
+                result = failure(normalize(path), text, t);
+                updated = commit(result);
+                errors = 1;
+                ran = true;
+            } finally {
+                if (updated) {
+                    server.refreshDiagnostics();
+                }
+                if (fullScope) {
+                    if (noEngine) {
+                        server.showMessage(MessageType.Warning,
+                                "Workspace compile skipped: no Drools engine on the project classpath");
+                    } else if (ran) {
+                        server.showMessage(errors > 0 ? MessageType.Warning : MessageType.Info,
+                                summaryMessage(errors, warnings, result));
+                    }
+                }
+                buildInFlight.set(false);
+                PendingBuild queued = pending.getAndSet(null);
+                if (queued != null) {
+                    trigger(queued.path, queued.text, queued.fullScope);
+                }
+            }
+        });
+    }
+
+    private static String summaryMessage(int errors, int warnings, Compilation result) {
+        String base = "Workspace compile finished: " + errors + " error(s), " + warnings + " warning(s)";
+        if (errors == 0 || result == null) {
+            return base;
+        }
+        return base + " in " + joinFileNames(result.filesWithErrors());
+    }
+
+    private static String joinFileNames(List<String> files) {
+        StringBuilder names = new StringBuilder();
+        int shown = Math.min(3, files.size());
+        for (int i = 0; i < shown; i++) {
+            if (i > 0) {
+                names.append(", ");
+            }
+            names.append(files.get(i));
+        }
+        if (files.size() > 3) {
+            names.append(" and ").append(files.size() - 3).append(" more");
+        }
+        return names.toString();
+    }
+
+    private Compilation compile(Path saved, String text, boolean fullScope, Engine target) throws Exception {
+        Path root = rootFor(saved);
+        Map<Path, String> sources = new LinkedHashMap<>();
+        Set<Path> skipped = new LinkedHashSet<>();
+        for (Path sibling : scope(saved, fullScope)) {
+            Path normalized = normalize(sibling);
+            if (normalized.equals(saved)) {
+                continue;
+            }
+            String siblingText = openText.apply(normalized);
+            if (siblingText == null) {
+                siblingText = readQuietly(normalized);
+            }
+            if (siblingText == null) {
+                skipped.add(normalized);
+                continue;
+            }
+            if (hasErrors(DRLDiagnosticHelper.parse(siblingText).diagnostics)) {
+                logger.fine(() -> "Leaving " + normalized + " out of the compile, it has syntax errors");
+                skipped.add(normalized);
+                continue;
+            }
+            sources.put(normalized, siblingText);
+        }
+        if (fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.fine(() -> "Leaving " + saved + " out of the compile, it has syntax errors");
+            skipped.add(saved);
+        } else {
+            sources.put(saved, text);
+        }
+
+        Map<String, Path> fileByKfsPath = new LinkedHashMap<>();
+        Map<String, String> drlByKfsPath = new LinkedHashMap<>();
+        for (Map.Entry<Path, String> entry : sources.entrySet()) {
+            String kfsPath = kfsPathFor(root, entry.getKey(), fileByKfsPath.size());
+            fileByKfsPath.put(kfsPath, entry.getKey());
+            drlByKfsPath.put(kfsPath, entry.getValue());
+        }
+        logger.info(() -> "Compiling " + sources.size() + " DRL file(s) for " + saved
+                + (fullScope ? " (whole workspace)" : ""));
+
+        List<Map<String, Object>> messages = target.build(drlByKfsPath);
+
+        Map<Path, List<Diagnostic>> perFile = new LinkedHashMap<>();
+        for (Path file : sources.keySet()) {
+            perFile.put(file, new ArrayList<>());
+        }
+        Path fallback = sources.containsKey(saved) ? saved : sources.keySet().stream().findFirst().orElse(null);
+        if (fallback != null) {
+            for (Map<String, Object> m : messages) {
+                Path file = fileFor(m.get("path"), fileByKfsPath, fallback);
+                List<Diagnostic> bucket = perFile.get(file);
+                if (bucket == null) {
+                    file = fallback;
+                    bucket = perFile.get(fallback);
+                }
+                String messageText = m.get("text") == null ? "" : String.valueOf(m.get("text"));
+                Diagnostic d = new Diagnostic();
+                d.setSeverity(severityOf(m.get("level")));
+                d.setSource(SOURCE);
+                d.setMessage(messageText);
+                d.setRange(BuildMessageRanges.rangeFor(messageText, intOf(m.get("line")), intOf(m.get("column")),
+                        sources.get(file)));
+                bucket.add(d);
+            }
+        }
+        perFile.replaceAll((file, list) -> cap(dedupe(dropCascades(list))));
+        return new Compilation(sources, perFile, skipped);
+    }
+
+    private Compilation failure(Path current, String text, Throwable t) {
+        Diagnostic d = new Diagnostic();
+        d.setSource(SOURCE);
+        String missing = missingClass(t);
+        if (missing != null) {
+            d.setSeverity(DiagnosticSeverity.Warning);
+            d.setMessage("The Drools compile needs the project's compiled classes: " + missing
+                    + " is not on the classpath. Build the project, then save again.");
+        } else {
+            d.setSeverity(DiagnosticSeverity.Error);
+            d.setMessage("Drools compile failed: " + summarize(t));
+        }
+        d.setRange(BuildMessageRanges.firstRuleRange(text));
+        Map<Path, List<Diagnostic>> perFile = new LinkedHashMap<>();
+        perFile.put(current, List.of(d));
+        return new Compilation(Map.of(current, text), perFile, Set.of());
+    }
+
+    private boolean commit(Compilation result) {
+        boolean[] updated = {false};
+        result.perFile.forEach((file, list) -> {
+            String compiled = result.sources.get(file);
+            cached.compute(file, (k, previous) -> {
+                String now = openText.apply(file);
+                if (now != null && !now.equals(compiled)) {
+                    return previous;
+                }
+                updated[0] = true;
+                return list.isEmpty() ? null : List.copyOf(list);
+            });
+        });
+        for (Path file : result.skipped()) {
+            if (cached.remove(file) != null) {
+                updated[0] = true;
+            }
+        }
+        return updated[0];
+    }
+
+    private List<Path> scope(Path current, boolean fullScope) {
+        List<Path> files;
+        if (!fullScope) {
+            files = WorkspaceSiblingResolvers.active().resolveSiblings(current);
+        } else {
+            files = WorkspaceSiblingResolvers.active().workspaceDrlFiles();
+            if (files.isEmpty()) {
+                files = WorkspaceScan.of(rootFor(current)).drlFiles();
+            }
+        }
+        return withoutBuildOutput(files);
+    }
+
+    /** Build output holds copies of the rules; compiling them next to their sources only duplicates every rule. */
+    private List<Path> withoutBuildOutput(List<Path> files) {
+        List<Path> outputDirs = new ArrayList<>();
+        for (Path dir : server.getBuildOutputDirs()) {
+            outputDirs.add(normalize(dir));
+        }
+        if (outputDirs.isEmpty()) {
+            return files;
+        }
+        List<Path> kept = new ArrayList<>(files.size());
+        for (Path file : files) {
+            Path normalized = normalize(file);
+            if (outputDirs.stream().noneMatch(normalized::startsWith)) {
+                kept.add(file);
+            }
+        }
+        return kept;
+    }
+
+    private Path rootFor(Path current) {
+        Path root = server.workspaceRoot();
+        return root != null ? normalize(root) : current.getParent();
+    }
+
+    static String kfsPathFor(Path root, Path file, int index) {
+        Path abs = normalize(file);
+        if (root != null) {
+            Path r = normalize(root);
+            if (abs.startsWith(r)) {
+                return KFS_PREFIX + r.relativize(abs).toString().replace('\\', '/');
+            }
+        }
+        return KFS_PREFIX + "external/" + index + "/" + abs.getFileName();
+    }
+
+    private static Path fileFor(Object pathValue, Map<String, Path> fileByKfsPath, Path fallback) {
+        if (pathValue == null) {
+            return fallback;
+        }
+        String p = String.valueOf(pathValue).replace('\\', '/');
+        if (p.isBlank()) {
+            return fallback;
+        }
+        Path exact = fileByKfsPath.get(p);
+        if (exact != null) {
+            return exact;
+        }
+        for (Map.Entry<String, Path> entry : fileByKfsPath.entrySet()) {
+            String key = entry.getKey();
+            if (p.endsWith("/" + key) || key.endsWith("/" + p) || p.equals(key)) {
+                return entry.getValue();
+            }
+        }
+        return fallback;
+    }
+
+    static List<Diagnostic> merge(List<Diagnostic> fast, List<Diagnostic> compiled) {
+        if (compiled == null || compiled.isEmpty()) {
+            return fast == null ? List.of() : fast;
+        }
+        List<Diagnostic> merged = new ArrayList<>();
+        Set<String> seen = new HashSet<>();
+        if (fast != null) {
+            for (Diagnostic d : fast) {
+                merged.add(d);
+                seen.add(mergeKey(d));
+            }
+        }
+        for (Diagnostic d : compiled) {
+            if (seen.add(mergeKey(d))) {
+                merged.add(d);
+            }
+        }
+        return merged;
+    }
+
+    private static String mergeKey(Diagnostic d) {
+        Range r = d.getRange();
+        String where = r == null ? "" : r.getStart().getLine() + ":" + r.getStart().getCharacter() + "-"
+                + r.getEnd().getLine() + ":" + r.getEnd().getCharacter();
+        return where + "|" + d.getSeverity() + "|" + d.getMessage();
+    }
+
+    private static List<Diagnostic> dropCascades(List<Diagnostic> in) {
+        boolean hasRootError = in.stream().anyMatch(d -> d.getSeverity() == DiagnosticSeverity.Error
+                && (d.getMessage() == null || !NOISY_CASCADE.matcher(d.getMessage()).find()));
+        if (!hasRootError) {
+            return in;
+        }
+        List<Diagnostic> out = new ArrayList<>(in.size());
+        for (Diagnostic d : in) {
+            if (d.getMessage() == null || !NOISY_CASCADE.matcher(d.getMessage()).find()) {
+                out.add(d);
+            }
+        }

Review Comment:
   A follow-on type message is now dropped only when a root error sits in the same rule. Outside every rule it is still weighed against the whole file.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "drccrd (via GitHub)" <gi...@apache.org>.
drccrd commented on code in PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#discussion_r4166643582


##########
packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/CompileDiagnostics.java:
##########
@@ -0,0 +1,645 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.lsp.server;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import org.drools.completion.DRLDiagnosticHelper;
+import org.drools.completion.WorkspaceScan;
+import org.drools.completion.WorkspaceSiblingResolvers;
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.DiagnosticSeverity;
+import org.eclipse.lsp4j.MessageType;
+import org.eclipse.lsp4j.Range;
+
+/**
+ * Compiles a saved DRL file together with its group through the project's own
+ * engine and keeps the engine's messages per file until the file is edited,
+ * saved again or regrouped. One compile runs at a time; a save during a compile
+ * queues one more and the latest wins.
+ */
+final class CompileDiagnostics {
+
+    static final String SOURCE = "drools";
+    static final String KFS_PREFIX = "src/main/resources/";
+    static final int MAX_DIAGNOSTICS_PER_FILE = 500;
+    static final String NOT_BUILT =
+            "Compile diagnostics need the project's compiled classes. Build the project, then save again.";
+
+    private static final Pattern NOISY_CASCADE = Pattern.compile(
+            "(?i)(unknown\\s*type|cannot\\s*resolve\\s*type|unresolved\\s*type|unable\\s*to\\s*resolve\\s*type|class\\s*not\\s*found)");
+    private static final Logger logger = Logger.getLogger(CompileDiagnostics.class.getName());
+
+    private final DroolsLspServer server;
+    private final Function<Path, String> openText;
+    private final Map<Path, List<Diagnostic>> cached = new ConcurrentHashMap<>();
+    private final AtomicBoolean buildInFlight = new AtomicBoolean(false);
+    private final AtomicReference<PendingBuild> pending = new AtomicReference<>();
+    private final ExecutorService gateExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile-gate"));
+    private final ExecutorService buildExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile"));
+    private volatile boolean onSave = true;
+    private final AtomicReference<Engine> engine = new AtomicReference<>();
+    private final AtomicBoolean notBuiltNoticeShown = new AtomicBoolean(false);
+
+    CompileDiagnostics(DroolsLspServer server, Function<Path, String> openText) {
+        this.server = server;
+        this.openText = openText;
+    }
+
+    boolean onSave() {
+        return onSave;
+    }
+
+    void setOnSave(boolean onSave) {
+        this.onSave = onSave;
+    }
+
+    Engine engine() {
+        return engine.get();
+    }
+
+    /** Installs the engine; the previous one is closed once the build in flight, if any, has finished. */
+    synchronized void setEngine(Engine next) {
+        if (buildExecutor.isShutdown()) {
+            next.close();
+            return;
+        }
+        Engine previous = engine.getAndSet(next);
+        if (previous == null || previous.available() != next.available()) {
+            logger.info(next.available()
+                    ? "Drools engine found on the project classpath; compile diagnostics are on"
+                    : "No usable Drools engine on the project classpath (needs drools-compiler and drools-mvel); "
+                            + "compile diagnostics are off");
+        }
+        if (previous != null && previous != next) {
+            buildExecutor.submit(previous::close);
+        }
+    }
+
+    /** Releases the engine and stops both executors; the close runs after any build in flight. */
+    synchronized void shutdown() {
+        Engine current = engine.getAndSet(null);
+        if (current != null) {
+            buildExecutor.submit(current::close);
+        }
+        gateExecutor.shutdown();
+        buildExecutor.shutdown();
+    }
+
+    List<Diagnostic> cachedFor(Path path) {
+        return path == null ? List.of() : cached.getOrDefault(normalize(path), List.of());
+    }
+
+    void invalidate(Path path) {
+        if (path != null) {
+            cached.remove(normalize(path));
+        }
+    }
+
+    void invalidateAll() {
+        cached.clear();
+    }
+
+    void onDidSave(Path path, String text) {
+        if (!onSave || path == null || text == null) {
+            return;
+        }
+        trigger(path, text, false);
+    }
+
+    void rebuildWorkspace(Path path) {
+        if (path == null) {
+            return;
+        }
+        String text = openText.apply(normalize(path));
+        if (text == null) {
+            text = readQuietly(path);
+        }
+        if (text == null) {
+            server.showMessage(MessageType.Warning, "Workspace compile skipped: cannot read " + path);
+            return;
+        }
+        trigger(path, text, true);
+    }
+
+    private void trigger(Path path, String text, boolean fullScope) {
+        gateExecutor.submit(() -> triggerNow(path, text, fullScope));
+    }
+
+    void triggerNow(Path path, String text, boolean fullScope) {
+        Engine current = engine.get();
+        if (current == null || !current.available()) {
+            logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: no Drools engine on the project classpath");
+            }
+            return;
+        }
+        if (server.projectClassesMissing()) {
+            if (notBuiltNoticeShown.compareAndSet(false, true)) {
+                logger.info(NOT_BUILT);
+                server.showMessage(MessageType.Info, NOT_BUILT);
+            }
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: the project has not been built");
+            }
+            return;
+        }
+        notBuiltNoticeShown.set(false);
+        if (!fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.info("Compile skipped, " + path + " has syntax errors");
+            return;
+        }
+        if (!buildInFlight.compareAndSet(false, true)) {
+            pending.accumulateAndGet(new PendingBuild(path, text, fullScope),
+                    (old, next) -> old != null && old.fullScope && !next.fullScope
+                            ? new PendingBuild(next.path, next.text, true)
+                            : next);
+            drainPendingIfUnclaimed();
+            return;
+        }
+        submit(path, text, fullScope);
+    }
+
+    private void drainPendingIfUnclaimed() {
+        while (pending.get() != null) {
+            if (!buildInFlight.compareAndSet(false, true)) {
+                return;
+            }
+            PendingBuild queued = pending.getAndSet(null);
+            if (queued != null) {
+                submit(queued.path, queued.text, queued.fullScope);
+                return;
+            }
+            buildInFlight.set(false);
+        }
+    }
+
+    private void submit(Path path, String text, boolean fullScope) {
+        buildExecutor.submit(() -> {
+            Engine target = engine.get();
+            boolean updated = false;
+            boolean ran = false;
+            boolean noEngine = false;
+            int errors = 0;
+            int warnings = 0;
+            Compilation result = null;
+            try {
+                if (target == null || !target.available()) {
+                    noEngine = true;
+                    logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+                    return;
+                }
+                if (fullScope) {
+                    server.showMessage(MessageType.Info, "Compiling every DRL file in the workspace");
+                }
+                result = compile(normalize(path), text, fullScope, target);
+                updated = commit(result);
+                errors = result.count(DiagnosticSeverity.Error);
+                warnings = result.count(DiagnosticSeverity.Warning);
+                ran = true;
+            } catch (Throwable t) {
+                logger.log(Level.WARNING, "Drools compile failed for " + path, t);
+                result = failure(normalize(path), text, t);
+                updated = commit(result);
+                errors = 1;
+                ran = true;
+            } finally {
+                if (updated) {
+                    server.refreshDiagnostics();
+                }
+                if (fullScope) {
+                    if (noEngine) {
+                        server.showMessage(MessageType.Warning,
+                                "Workspace compile skipped: no Drools engine on the project classpath");
+                    } else if (ran) {
+                        server.showMessage(errors > 0 ? MessageType.Warning : MessageType.Info,
+                                summaryMessage(errors, warnings, result));
+                    }
+                }
+                buildInFlight.set(false);
+                PendingBuild queued = pending.getAndSet(null);
+                if (queued != null) {
+                    trigger(queued.path, queued.text, queued.fullScope);
+                }
+            }
+        });
+    }
+
+    private static String summaryMessage(int errors, int warnings, Compilation result) {
+        String base = "Workspace compile finished: " + errors + " error(s), " + warnings + " warning(s)";
+        if (errors == 0 || result == null) {
+            return base;
+        }
+        return base + " in " + joinFileNames(result.filesWithErrors());
+    }
+
+    private static String joinFileNames(List<String> files) {
+        StringBuilder names = new StringBuilder();
+        int shown = Math.min(3, files.size());
+        for (int i = 0; i < shown; i++) {
+            if (i > 0) {
+                names.append(", ");
+            }
+            names.append(files.get(i));
+        }
+        if (files.size() > 3) {
+            names.append(" and ").append(files.size() - 3).append(" more");
+        }
+        return names.toString();
+    }
+
+    private Compilation compile(Path saved, String text, boolean fullScope, Engine target) throws Exception {
+        Path root = rootFor(saved);
+        Map<Path, String> sources = new LinkedHashMap<>();
+        Set<Path> skipped = new LinkedHashSet<>();
+        for (Path sibling : scope(saved, fullScope)) {
+            Path normalized = normalize(sibling);
+            if (normalized.equals(saved)) {
+                continue;
+            }
+            String siblingText = openText.apply(normalized);
+            if (siblingText == null) {
+                siblingText = readQuietly(normalized);
+            }
+            if (siblingText == null) {
+                skipped.add(normalized);
+                continue;
+            }
+            if (hasErrors(DRLDiagnosticHelper.parse(siblingText).diagnostics)) {
+                logger.fine(() -> "Leaving " + normalized + " out of the compile, it has syntax errors");
+                skipped.add(normalized);
+                continue;
+            }
+            sources.put(normalized, siblingText);
+        }
+        if (fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.fine(() -> "Leaving " + saved + " out of the compile, it has syntax errors");
+            skipped.add(saved);
+        } else {
+            sources.put(saved, text);
+        }
+
+        Map<String, Path> fileByKfsPath = new LinkedHashMap<>();
+        Map<String, String> drlByKfsPath = new LinkedHashMap<>();
+        for (Map.Entry<Path, String> entry : sources.entrySet()) {
+            String kfsPath = kfsPathFor(root, entry.getKey(), fileByKfsPath.size());
+            fileByKfsPath.put(kfsPath, entry.getKey());
+            drlByKfsPath.put(kfsPath, entry.getValue());
+        }
+        logger.info(() -> "Compiling " + sources.size() + " DRL file(s) for " + saved
+                + (fullScope ? " (whole workspace)" : ""));
+
+        List<Map<String, Object>> messages = target.build(drlByKfsPath);
+
+        Map<Path, List<Diagnostic>> perFile = new LinkedHashMap<>();
+        for (Path file : sources.keySet()) {
+            perFile.put(file, new ArrayList<>());
+        }
+        Path fallback = sources.containsKey(saved) ? saved : sources.keySet().stream().findFirst().orElse(null);
+        if (fallback != null) {
+            for (Map<String, Object> m : messages) {
+                Path file = fileFor(m.get("path"), fileByKfsPath, fallback);
+                List<Diagnostic> bucket = perFile.get(file);
+                if (bucket == null) {
+                    file = fallback;
+                    bucket = perFile.get(fallback);
+                }
+                String messageText = m.get("text") == null ? "" : String.valueOf(m.get("text"));
+                Diagnostic d = new Diagnostic();
+                d.setSeverity(severityOf(m.get("level")));
+                d.setSource(SOURCE);
+                d.setMessage(messageText);
+                d.setRange(BuildMessageRanges.rangeFor(messageText, intOf(m.get("line")), intOf(m.get("column")),
+                        sources.get(file)));
+                bucket.add(d);
+            }
+        }
+        perFile.replaceAll((file, list) -> cap(dedupe(dropCascades(list))));
+        return new Compilation(sources, perFile, skipped);
+    }
+
+    private Compilation failure(Path current, String text, Throwable t) {
+        Diagnostic d = new Diagnostic();
+        d.setSource(SOURCE);
+        String missing = missingClass(t);
+        if (missing != null) {
+            d.setSeverity(DiagnosticSeverity.Warning);
+            d.setMessage("The Drools compile needs the project's compiled classes: " + missing
+                    + " is not on the classpath. Build the project, then save again.");
+        } else {
+            d.setSeverity(DiagnosticSeverity.Error);
+            d.setMessage("Drools compile failed: " + summarize(t));
+        }
+        d.setRange(BuildMessageRanges.firstRuleRange(text));
+        Map<Path, List<Diagnostic>> perFile = new LinkedHashMap<>();
+        perFile.put(current, List.of(d));
+        return new Compilation(Map.of(current, text), perFile, Set.of());
+    }
+
+    private boolean commit(Compilation result) {
+        boolean[] updated = {false};
+        result.perFile.forEach((file, list) -> {
+            String compiled = result.sources.get(file);
+            cached.compute(file, (k, previous) -> {
+                String now = openText.apply(file);
+                if (now != null && !now.equals(compiled)) {
+                    return previous;
+                }
+                updated[0] = true;
+                return list.isEmpty() ? null : List.copyOf(list);
+            });
+        });
+        for (Path file : result.skipped()) {
+            if (cached.remove(file) != null) {
+                updated[0] = true;
+            }
+        }
+        return updated[0];
+    }
+
+    private List<Path> scope(Path current, boolean fullScope) {
+        List<Path> files;
+        if (!fullScope) {
+            files = WorkspaceSiblingResolvers.active().resolveSiblings(current);
+        } else {
+            files = WorkspaceSiblingResolvers.active().workspaceDrlFiles();
+            if (files.isEmpty()) {
+                files = WorkspaceScan.of(rootFor(current)).drlFiles();

Review Comment:
   An empty enumeration means there is nothing to rebuild from the client's point of view; the walk only serves resolvers that keep no file view. Distinguishing the two would complicate the public SPI for a degenerate case.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "yesamer (via GitHub)" <gi...@apache.org>.
yesamer commented on PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#issuecomment-6098054697

   Hi @tkobayas and @drccrd, considering your notable effort on the DRL VsCode extension, I think it would be nice to write a blog post about it in https://kie.apache.org/blog, WDYT? Thanks.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "drccrd (via GitHub)" <gi...@apache.org>.
drccrd commented on code in PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#discussion_r4166651686


##########
packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/CompileDiagnostics.java:
##########
@@ -0,0 +1,645 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.lsp.server;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import org.drools.completion.DRLDiagnosticHelper;
+import org.drools.completion.WorkspaceScan;
+import org.drools.completion.WorkspaceSiblingResolvers;
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.DiagnosticSeverity;
+import org.eclipse.lsp4j.MessageType;
+import org.eclipse.lsp4j.Range;
+
+/**
+ * Compiles a saved DRL file together with its group through the project's own
+ * engine and keeps the engine's messages per file until the file is edited,
+ * saved again or regrouped. One compile runs at a time; a save during a compile
+ * queues one more and the latest wins.
+ */
+final class CompileDiagnostics {
+
+    static final String SOURCE = "drools";
+    static final String KFS_PREFIX = "src/main/resources/";
+    static final int MAX_DIAGNOSTICS_PER_FILE = 500;
+    static final String NOT_BUILT =
+            "Compile diagnostics need the project's compiled classes. Build the project, then save again.";
+
+    private static final Pattern NOISY_CASCADE = Pattern.compile(
+            "(?i)(unknown\\s*type|cannot\\s*resolve\\s*type|unresolved\\s*type|unable\\s*to\\s*resolve\\s*type|class\\s*not\\s*found)");
+    private static final Logger logger = Logger.getLogger(CompileDiagnostics.class.getName());
+
+    private final DroolsLspServer server;
+    private final Function<Path, String> openText;
+    private final Map<Path, List<Diagnostic>> cached = new ConcurrentHashMap<>();
+    private final AtomicBoolean buildInFlight = new AtomicBoolean(false);
+    private final AtomicReference<PendingBuild> pending = new AtomicReference<>();
+    private final ExecutorService gateExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile-gate"));
+    private final ExecutorService buildExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile"));
+    private volatile boolean onSave = true;
+    private final AtomicReference<Engine> engine = new AtomicReference<>();
+    private final AtomicBoolean notBuiltNoticeShown = new AtomicBoolean(false);
+
+    CompileDiagnostics(DroolsLspServer server, Function<Path, String> openText) {
+        this.server = server;
+        this.openText = openText;
+    }
+
+    boolean onSave() {
+        return onSave;
+    }
+
+    void setOnSave(boolean onSave) {
+        this.onSave = onSave;
+    }
+
+    Engine engine() {
+        return engine.get();
+    }
+
+    /** Installs the engine; the previous one is closed once the build in flight, if any, has finished. */
+    synchronized void setEngine(Engine next) {
+        if (buildExecutor.isShutdown()) {
+            next.close();
+            return;
+        }
+        Engine previous = engine.getAndSet(next);
+        if (previous == null || previous.available() != next.available()) {
+            logger.info(next.available()
+                    ? "Drools engine found on the project classpath; compile diagnostics are on"
+                    : "No usable Drools engine on the project classpath (needs drools-compiler and drools-mvel); "
+                            + "compile diagnostics are off");
+        }
+        if (previous != null && previous != next) {
+            buildExecutor.submit(previous::close);
+        }
+    }
+
+    /** Releases the engine and stops both executors; the close runs after any build in flight. */
+    synchronized void shutdown() {
+        Engine current = engine.getAndSet(null);
+        if (current != null) {
+            buildExecutor.submit(current::close);
+        }
+        gateExecutor.shutdown();
+        buildExecutor.shutdown();
+    }
+
+    List<Diagnostic> cachedFor(Path path) {
+        return path == null ? List.of() : cached.getOrDefault(normalize(path), List.of());
+    }
+
+    void invalidate(Path path) {
+        if (path != null) {
+            cached.remove(normalize(path));
+        }
+    }
+
+    void invalidateAll() {
+        cached.clear();
+    }
+
+    void onDidSave(Path path, String text) {
+        if (!onSave || path == null || text == null) {
+            return;
+        }
+        trigger(path, text, false);
+    }
+
+    void rebuildWorkspace(Path path) {
+        if (path == null) {
+            return;
+        }
+        String text = openText.apply(normalize(path));
+        if (text == null) {
+            text = readQuietly(path);
+        }
+        if (text == null) {
+            server.showMessage(MessageType.Warning, "Workspace compile skipped: cannot read " + path);
+            return;
+        }
+        trigger(path, text, true);
+    }
+
+    private void trigger(Path path, String text, boolean fullScope) {
+        gateExecutor.submit(() -> triggerNow(path, text, fullScope));
+    }
+
+    void triggerNow(Path path, String text, boolean fullScope) {
+        Engine current = engine.get();
+        if (current == null || !current.available()) {
+            logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: no Drools engine on the project classpath");
+            }
+            return;
+        }
+        if (server.projectClassesMissing()) {
+            if (notBuiltNoticeShown.compareAndSet(false, true)) {
+                logger.info(NOT_BUILT);
+                server.showMessage(MessageType.Info, NOT_BUILT);
+            }
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: the project has not been built");
+            }
+            return;
+        }
+        notBuiltNoticeShown.set(false);
+        if (!fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.info("Compile skipped, " + path + " has syntax errors");
+            return;
+        }
+        if (!buildInFlight.compareAndSet(false, true)) {
+            pending.accumulateAndGet(new PendingBuild(path, text, fullScope),
+                    (old, next) -> old != null && old.fullScope && !next.fullScope
+                            ? new PendingBuild(next.path, next.text, true)
+                            : next);
+            drainPendingIfUnclaimed();
+            return;
+        }
+        submit(path, text, fullScope);
+    }
+
+    private void drainPendingIfUnclaimed() {
+        while (pending.get() != null) {
+            if (!buildInFlight.compareAndSet(false, true)) {
+                return;
+            }
+            PendingBuild queued = pending.getAndSet(null);
+            if (queued != null) {
+                submit(queued.path, queued.text, queued.fullScope);
+                return;
+            }
+            buildInFlight.set(false);
+        }
+    }
+
+    private void submit(Path path, String text, boolean fullScope) {
+        buildExecutor.submit(() -> {
+            Engine target = engine.get();
+            boolean updated = false;
+            boolean ran = false;
+            boolean noEngine = false;
+            int errors = 0;
+            int warnings = 0;
+            Compilation result = null;
+            try {
+                if (target == null || !target.available()) {
+                    noEngine = true;
+                    logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+                    return;
+                }
+                if (fullScope) {
+                    server.showMessage(MessageType.Info, "Compiling every DRL file in the workspace");
+                }
+                result = compile(normalize(path), text, fullScope, target);
+                updated = commit(result);
+                errors = result.count(DiagnosticSeverity.Error);
+                warnings = result.count(DiagnosticSeverity.Warning);
+                ran = true;
+            } catch (Throwable t) {
+                logger.log(Level.WARNING, "Drools compile failed for " + path, t);
+                result = failure(normalize(path), text, t);
+                updated = commit(result);
+                errors = 1;
+                ran = true;
+            } finally {
+                if (updated) {
+                    server.refreshDiagnostics();
+                }
+                if (fullScope) {
+                    if (noEngine) {
+                        server.showMessage(MessageType.Warning,
+                                "Workspace compile skipped: no Drools engine on the project classpath");
+                    } else if (ran) {
+                        server.showMessage(errors > 0 ? MessageType.Warning : MessageType.Info,
+                                summaryMessage(errors, warnings, result));
+                    }
+                }
+                buildInFlight.set(false);
+                PendingBuild queued = pending.getAndSet(null);
+                if (queued != null) {
+                    trigger(queued.path, queued.text, queued.fullScope);
+                }
+            }
+        });
+    }
+
+    private static String summaryMessage(int errors, int warnings, Compilation result) {
+        String base = "Workspace compile finished: " + errors + " error(s), " + warnings + " warning(s)";
+        if (errors == 0 || result == null) {
+            return base;
+        }
+        return base + " in " + joinFileNames(result.filesWithErrors());
+    }
+
+    private static String joinFileNames(List<String> files) {
+        StringBuilder names = new StringBuilder();
+        int shown = Math.min(3, files.size());
+        for (int i = 0; i < shown; i++) {
+            if (i > 0) {
+                names.append(", ");
+            }
+            names.append(files.get(i));
+        }
+        if (files.size() > 3) {
+            names.append(" and ").append(files.size() - 3).append(" more");
+        }
+        return names.toString();
+    }
+
+    private Compilation compile(Path saved, String text, boolean fullScope, Engine target) throws Exception {
+        Path root = rootFor(saved);
+        Map<Path, String> sources = new LinkedHashMap<>();
+        Set<Path> skipped = new LinkedHashSet<>();
+        for (Path sibling : scope(saved, fullScope)) {
+            Path normalized = normalize(sibling);
+            if (normalized.equals(saved)) {
+                continue;
+            }
+            String siblingText = openText.apply(normalized);
+            if (siblingText == null) {
+                siblingText = readQuietly(normalized);
+            }
+            if (siblingText == null) {
+                skipped.add(normalized);
+                continue;
+            }
+            if (hasErrors(DRLDiagnosticHelper.parse(siblingText).diagnostics)) {
+                logger.fine(() -> "Leaving " + normalized + " out of the compile, it has syntax errors");
+                skipped.add(normalized);
+                continue;
+            }
+            sources.put(normalized, siblingText);
+        }
+        if (fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.fine(() -> "Leaving " + saved + " out of the compile, it has syntax errors");
+            skipped.add(saved);
+        } else {
+            sources.put(saved, text);
+        }
+
+        Map<String, Path> fileByKfsPath = new LinkedHashMap<>();
+        Map<String, String> drlByKfsPath = new LinkedHashMap<>();
+        for (Map.Entry<Path, String> entry : sources.entrySet()) {
+            String kfsPath = kfsPathFor(root, entry.getKey(), fileByKfsPath.size());
+            fileByKfsPath.put(kfsPath, entry.getKey());
+            drlByKfsPath.put(kfsPath, entry.getValue());
+        }
+        logger.info(() -> "Compiling " + sources.size() + " DRL file(s) for " + saved
+                + (fullScope ? " (whole workspace)" : ""));
+
+        List<Map<String, Object>> messages = target.build(drlByKfsPath);
+
+        Map<Path, List<Diagnostic>> perFile = new LinkedHashMap<>();
+        for (Path file : sources.keySet()) {
+            perFile.put(file, new ArrayList<>());
+        }
+        Path fallback = sources.containsKey(saved) ? saved : sources.keySet().stream().findFirst().orElse(null);
+        if (fallback != null) {
+            for (Map<String, Object> m : messages) {
+                Path file = fileFor(m.get("path"), fileByKfsPath, fallback);
+                List<Diagnostic> bucket = perFile.get(file);
+                if (bucket == null) {
+                    file = fallback;
+                    bucket = perFile.get(fallback);
+                }
+                String messageText = m.get("text") == null ? "" : String.valueOf(m.get("text"));
+                Diagnostic d = new Diagnostic();
+                d.setSeverity(severityOf(m.get("level")));
+                d.setSource(SOURCE);
+                d.setMessage(messageText);
+                d.setRange(BuildMessageRanges.rangeFor(messageText, intOf(m.get("line")), intOf(m.get("column")),
+                        sources.get(file)));
+                bucket.add(d);
+            }
+        }
+        perFile.replaceAll((file, list) -> cap(dedupe(dropCascades(list))));
+        return new Compilation(sources, perFile, skipped);
+    }
+
+    private Compilation failure(Path current, String text, Throwable t) {
+        Diagnostic d = new Diagnostic();
+        d.setSource(SOURCE);
+        String missing = missingClass(t);
+        if (missing != null) {
+            d.setSeverity(DiagnosticSeverity.Warning);
+            d.setMessage("The Drools compile needs the project's compiled classes: " + missing
+                    + " is not on the classpath. Build the project, then save again.");
+        } else {
+            d.setSeverity(DiagnosticSeverity.Error);
+            d.setMessage("Drools compile failed: " + summarize(t));
+        }
+        d.setRange(BuildMessageRanges.firstRuleRange(text));
+        Map<Path, List<Diagnostic>> perFile = new LinkedHashMap<>();
+        perFile.put(current, List.of(d));
+        return new Compilation(Map.of(current, text), perFile, Set.of());
+    }
+
+    private boolean commit(Compilation result) {
+        boolean[] updated = {false};
+        result.perFile.forEach((file, list) -> {
+            String compiled = result.sources.get(file);
+            cached.compute(file, (k, previous) -> {
+                String now = openText.apply(file);
+                if (now != null && !now.equals(compiled)) {
+                    return previous;
+                }
+                updated[0] = true;
+                return list.isEmpty() ? null : List.copyOf(list);

Review Comment:
   outdated results are now discarded



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "drccrd (via GitHub)" <gi...@apache.org>.
drccrd commented on code in PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#discussion_r4166637603


##########
packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/ProjectEngine.java:
##########
@@ -0,0 +1,216 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.lsp.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * The project's own Drools engine, driven through the bridge class.
+ *
+ * <p>The project loader sees the project's classpath and the platform classes
+ * only. The server's loader is deliberately not its parent: the server jar
+ * carries a DRL parser and an ANTLR runtime of its own, and the project's
+ * versions must win inside the engine. The bridge class is defined from this
+ * server's own jar into a child of the project loader, so its references to
+ * kie-api resolve against the project.
+ */
+final class ProjectEngine implements Engine {
+
+    static final String BRIDGE_CLASS = "org.drools.lsp.bridge.EngineBridge";
+    static final String COMPILER_CLASS = "org.drools.compiler.kie.builder.impl.KieServicesImpl";
+    static final String MVEL_CLASS = "org.drools.mvel.MVELConstraint";
+    private static final String BRIDGE_PACKAGE_PREFIX = "org.drools.lsp.bridge.";
+    private static final Logger logger = Logger.getLogger(ProjectEngine.class.getName());
+
+    private final Set<Path> entries;
+    private final URLClassLoader projectLoader;
+    private final Class<?> bridge;
+    private final Method build;
+    private volatile boolean closed;
+
+    private ProjectEngine(Set<Path> entries, URLClassLoader projectLoader, Class<?> bridge, Method build) {
+        this.entries = entries;
+        this.projectLoader = projectLoader;
+        this.bridge = bridge;
+        this.build = build;
+    }
+
+    static ProjectEngine over(Collection<Path> classpathEntries) {
+        Set<Path> entries = new LinkedHashSet<>(classpathEntries);
+        List<URL> urls = new ArrayList<>(entries.size());
+        for (Path entry : entries) {
+            try {
+                urls.add(entry.toUri().toURL());
+            } catch (MalformedURLException e) {
+                logger.fine(() -> "Skipping classpath entry " + entry + ": " + e.getMessage());
+            }
+        }
+        URLClassLoader projectLoader = new URLClassLoader("drools-lsp-project",
+                urls.toArray(new URL[0]), ClassLoader.getPlatformClassLoader());
+        Class<?> bridge = null;
+        Method build = null;
+        if (hasCompiler(projectLoader)) {
+            try {
+                bridge = Class.forName(BRIDGE_CLASS, true, new BridgeClassLoader(projectLoader));
+                build = bridge.getMethod("build", Map.class);
+            } catch (ReflectiveOperationException | LinkageError e) {
+                logger.log(Level.WARNING, "A Drools engine is on the project classpath but the compile bridge did not link", e);
+            }
+        }
+        ProjectEngine engine = new ProjectEngine(entries, projectLoader, bridge, build);
+        if (build == null) {
+            // Permanently unavailable: release the loader's open jar handles now
+            // rather than holding them until the next classpath change replaces it.
+            engine.close();
+        }
+        return engine;
+    }
+
+    Set<Path> entries() {
+        return entries;
+    }
+
+    @Override
+    public boolean available() {
+        return build != null && !closed;
+    }
+
+    @Override
+    public List<Map<String, Object>> build(Map<String, String> drlByPath) throws Exception {
+        if (!available()) {
+            throw new IllegalStateException("No Drools engine available");
+        }
+        AtomicReference<Object> outcome = new AtomicReference<>();
+        Thread worker = new Thread(() -> {
+            Thread.currentThread().setContextClassLoader(bridge.getClassLoader());
+            try {
+                outcome.set(build.invoke(null, drlByPath));
+            } catch (Throwable t) {
+                outcome.set(t);
+            }
+        }, "drools-lsp-engine-build");
+        worker.setDaemon(true);
+        worker.start();
+        worker.join();
+        Object result = outcome.get();
+        if (result instanceof InvocationTargetException e) {
+            Throwable cause = e.getCause();
+            if (cause instanceof Exception exception) {
+                throw exception;
+            }
+            throw e;
+        }
+        if (result instanceof Exception e) {
+            throw e;
+        }
+        if (result instanceof Error e) {
+            throw e;
+        }
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> messages = (List<Map<String, Object>>) result;
+        return messages;
+    }
+
+    @Override
+    public void close() {
+        closed = true;
+        try {
+            projectLoader.close();
+        } catch (IOException e) {
+            logger.log(Level.FINE, "Closing the project class loader failed", e);
+        }
+    }
+
+    boolean isClosed() {
+        return closed;
+    }
+
+    Class<?> bridgeClassForTest() {
+        return bridge;
+    }
+
+    ClassLoader projectLoaderForTest() {
+        return projectLoader;
+    }
+
+    private static boolean hasCompiler(ClassLoader projectLoader) {
+        try {
+            Class.forName("org.kie.api.KieServices", false, projectLoader);
+        } catch (ClassNotFoundException e) {
+            logger.fine("No Drools engine on the project classpath; compile diagnostics stay off until it changes");
+            return false;
+        }
+        try {
+            Class.forName(COMPILER_CLASS, false, projectLoader);
+        } catch (ClassNotFoundException e) {
+            logger.fine("kie-api is on the project classpath but drools-compiler is not; compile diagnostics stay off until it changes");
+            return false;
+        }
+        try {
+            Class.forName(MVEL_CLASS, false, projectLoader);
+        } catch (ClassNotFoundException e) {
+            logger.fine("drools-compiler is on the project classpath but drools-mvel is not; the classic build cannot "
+                    + "compile constraints, compile diagnostics stay off until it changes");
+            return false;
+        }
+        return true;
+    }

Review Comment:
   Covered the further error types with a warning about missing classpath



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "drccrd (via GitHub)" <gi...@apache.org>.
drccrd commented on PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#issuecomment-5935274787

   @tkobayas I'm pretty sure this is the last feature I have to port (along with #4007 ). I thought I was done with the formatter but I keep finding new features locally that haven't made it upstream yet...


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#discussion_r4162547240


##########
packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/ProjectEngine.java:
##########
@@ -0,0 +1,216 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.lsp.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * The project's own Drools engine, driven through the bridge class.
+ *
+ * <p>The project loader sees the project's classpath and the platform classes
+ * only. The server's loader is deliberately not its parent: the server jar
+ * carries a DRL parser and an ANTLR runtime of its own, and the project's
+ * versions must win inside the engine. The bridge class is defined from this
+ * server's own jar into a child of the project loader, so its references to
+ * kie-api resolve against the project.
+ */
+final class ProjectEngine implements Engine {
+
+    static final String BRIDGE_CLASS = "org.drools.lsp.bridge.EngineBridge";
+    static final String COMPILER_CLASS = "org.drools.compiler.kie.builder.impl.KieServicesImpl";
+    static final String MVEL_CLASS = "org.drools.mvel.MVELConstraint";
+    private static final String BRIDGE_PACKAGE_PREFIX = "org.drools.lsp.bridge.";
+    private static final Logger logger = Logger.getLogger(ProjectEngine.class.getName());
+
+    private final Set<Path> entries;
+    private final URLClassLoader projectLoader;
+    private final Class<?> bridge;
+    private final Method build;
+    private volatile boolean closed;
+
+    private ProjectEngine(Set<Path> entries, URLClassLoader projectLoader, Class<?> bridge, Method build) {
+        this.entries = entries;
+        this.projectLoader = projectLoader;
+        this.bridge = bridge;
+        this.build = build;
+    }
+
+    static ProjectEngine over(Collection<Path> classpathEntries) {
+        Set<Path> entries = new LinkedHashSet<>(classpathEntries);
+        List<URL> urls = new ArrayList<>(entries.size());
+        for (Path entry : entries) {
+            try {
+                urls.add(entry.toUri().toURL());
+            } catch (MalformedURLException e) {
+                logger.fine(() -> "Skipping classpath entry " + entry + ": " + e.getMessage());
+            }
+        }
+        URLClassLoader projectLoader = new URLClassLoader("drools-lsp-project",
+                urls.toArray(new URL[0]), ClassLoader.getPlatformClassLoader());
+        Class<?> bridge = null;
+        Method build = null;
+        if (hasCompiler(projectLoader)) {
+            try {
+                bridge = Class.forName(BRIDGE_CLASS, true, new BridgeClassLoader(projectLoader));
+                build = bridge.getMethod("build", Map.class);
+            } catch (ReflectiveOperationException | LinkageError e) {
+                logger.log(Level.WARNING, "A Drools engine is on the project classpath but the compile bridge did not link", e);
+            }
+        }
+        ProjectEngine engine = new ProjectEngine(entries, projectLoader, bridge, build);
+        if (build == null) {
+            // Permanently unavailable: release the loader's open jar handles now
+            // rather than holding them until the next classpath change replaces it.
+            engine.close();
+        }
+        return engine;
+    }
+
+    Set<Path> entries() {
+        return entries;
+    }
+
+    @Override
+    public boolean available() {
+        return build != null && !closed;
+    }
+
+    @Override
+    public List<Map<String, Object>> build(Map<String, String> drlByPath) throws Exception {
+        if (!available()) {
+            throw new IllegalStateException("No Drools engine available");
+        }
+        AtomicReference<Object> outcome = new AtomicReference<>();
+        Thread worker = new Thread(() -> {
+            Thread.currentThread().setContextClassLoader(bridge.getClassLoader());
+            try {
+                outcome.set(build.invoke(null, drlByPath));
+            } catch (Throwable t) {
+                outcome.set(t);
+            }
+        }, "drools-lsp-engine-build");
+        worker.setDaemon(true);
+        worker.start();
+        worker.join();
+        Object result = outcome.get();
+        if (result instanceof InvocationTargetException e) {
+            Throwable cause = e.getCause();
+            if (cause instanceof Exception exception) {
+                throw exception;
+            }
+            throw e;
+        }
+        if (result instanceof Exception e) {
+            throw e;
+        }
+        if (result instanceof Error e) {
+            throw e;
+        }
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> messages = (List<Map<String, Object>>) result;
+        return messages;
+    }
+
+    @Override
+    public void close() {
+        closed = true;
+        try {
+            projectLoader.close();
+        } catch (IOException e) {
+            logger.log(Level.FINE, "Closing the project class loader failed", e);
+        }
+    }
+
+    boolean isClosed() {
+        return closed;
+    }
+
+    Class<?> bridgeClassForTest() {
+        return bridge;
+    }
+
+    ClassLoader projectLoaderForTest() {
+        return projectLoader;
+    }
+
+    private static boolean hasCompiler(ClassLoader projectLoader) {
+        try {
+            Class.forName("org.kie.api.KieServices", false, projectLoader);
+        } catch (ClassNotFoundException e) {
+            logger.fine("No Drools engine on the project classpath; compile diagnostics stay off until it changes");
+            return false;
+        }
+        try {
+            Class.forName(COMPILER_CLASS, false, projectLoader);
+        } catch (ClassNotFoundException e) {
+            logger.fine("kie-api is on the project classpath but drools-compiler is not; compile diagnostics stay off until it changes");
+            return false;
+        }
+        try {
+            Class.forName(MVEL_CLASS, false, projectLoader);
+        } catch (ClassNotFoundException e) {
+            logger.fine("drools-compiler is on the project classpath but drools-mvel is not; the classic build cannot "
+                    + "compile constraints, compile diagnostics stay off until it changes");
+            return false;
+        }
+        return true;
+    }

Review Comment:
   These availability probes only handle an absent class. A partially resolved or incompatible project classpath can throw `NoClassDefFoundError`, `UnsupportedClassVersionError`, or another `LinkageError` while loading one of these classes; that escapes `ProjectEngine.over()` and can abort class-index initialization, violating the intended no-engine fallback. Treat linkage failures as an unavailable engine just like the bridge-linking block already does.



##########
packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/CompileDiagnostics.java:
##########
@@ -0,0 +1,645 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.lsp.server;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import org.drools.completion.DRLDiagnosticHelper;
+import org.drools.completion.WorkspaceScan;
+import org.drools.completion.WorkspaceSiblingResolvers;
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.DiagnosticSeverity;
+import org.eclipse.lsp4j.MessageType;
+import org.eclipse.lsp4j.Range;
+
+/**
+ * Compiles a saved DRL file together with its group through the project's own
+ * engine and keeps the engine's messages per file until the file is edited,
+ * saved again or regrouped. One compile runs at a time; a save during a compile
+ * queues one more and the latest wins.
+ */
+final class CompileDiagnostics {
+
+    static final String SOURCE = "drools";
+    static final String KFS_PREFIX = "src/main/resources/";
+    static final int MAX_DIAGNOSTICS_PER_FILE = 500;
+    static final String NOT_BUILT =
+            "Compile diagnostics need the project's compiled classes. Build the project, then save again.";
+
+    private static final Pattern NOISY_CASCADE = Pattern.compile(
+            "(?i)(unknown\\s*type|cannot\\s*resolve\\s*type|unresolved\\s*type|unable\\s*to\\s*resolve\\s*type|class\\s*not\\s*found)");
+    private static final Logger logger = Logger.getLogger(CompileDiagnostics.class.getName());
+
+    private final DroolsLspServer server;
+    private final Function<Path, String> openText;
+    private final Map<Path, List<Diagnostic>> cached = new ConcurrentHashMap<>();
+    private final AtomicBoolean buildInFlight = new AtomicBoolean(false);
+    private final AtomicReference<PendingBuild> pending = new AtomicReference<>();
+    private final ExecutorService gateExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile-gate"));
+    private final ExecutorService buildExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile"));
+    private volatile boolean onSave = true;
+    private final AtomicReference<Engine> engine = new AtomicReference<>();
+    private final AtomicBoolean notBuiltNoticeShown = new AtomicBoolean(false);
+
+    CompileDiagnostics(DroolsLspServer server, Function<Path, String> openText) {
+        this.server = server;
+        this.openText = openText;
+    }
+
+    boolean onSave() {
+        return onSave;
+    }
+
+    void setOnSave(boolean onSave) {
+        this.onSave = onSave;
+    }
+
+    Engine engine() {
+        return engine.get();
+    }
+
+    /** Installs the engine; the previous one is closed once the build in flight, if any, has finished. */
+    synchronized void setEngine(Engine next) {
+        if (buildExecutor.isShutdown()) {
+            next.close();
+            return;
+        }
+        Engine previous = engine.getAndSet(next);
+        if (previous == null || previous.available() != next.available()) {
+            logger.info(next.available()
+                    ? "Drools engine found on the project classpath; compile diagnostics are on"
+                    : "No usable Drools engine on the project classpath (needs drools-compiler and drools-mvel); "
+                            + "compile diagnostics are off");
+        }
+        if (previous != null && previous != next) {
+            buildExecutor.submit(previous::close);
+        }
+    }
+
+    /** Releases the engine and stops both executors; the close runs after any build in flight. */
+    synchronized void shutdown() {
+        Engine current = engine.getAndSet(null);
+        if (current != null) {
+            buildExecutor.submit(current::close);
+        }
+        gateExecutor.shutdown();
+        buildExecutor.shutdown();
+    }
+
+    List<Diagnostic> cachedFor(Path path) {
+        return path == null ? List.of() : cached.getOrDefault(normalize(path), List.of());
+    }
+
+    void invalidate(Path path) {
+        if (path != null) {
+            cached.remove(normalize(path));
+        }
+    }
+
+    void invalidateAll() {
+        cached.clear();
+    }
+
+    void onDidSave(Path path, String text) {
+        if (!onSave || path == null || text == null) {
+            return;
+        }
+        trigger(path, text, false);
+    }
+
+    void rebuildWorkspace(Path path) {
+        if (path == null) {
+            return;
+        }
+        String text = openText.apply(normalize(path));
+        if (text == null) {
+            text = readQuietly(path);
+        }
+        if (text == null) {
+            server.showMessage(MessageType.Warning, "Workspace compile skipped: cannot read " + path);
+            return;
+        }
+        trigger(path, text, true);
+    }
+
+    private void trigger(Path path, String text, boolean fullScope) {
+        gateExecutor.submit(() -> triggerNow(path, text, fullScope));
+    }
+
+    void triggerNow(Path path, String text, boolean fullScope) {
+        Engine current = engine.get();
+        if (current == null || !current.available()) {
+            logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: no Drools engine on the project classpath");
+            }
+            return;
+        }
+        if (server.projectClassesMissing()) {
+            if (notBuiltNoticeShown.compareAndSet(false, true)) {
+                logger.info(NOT_BUILT);
+                server.showMessage(MessageType.Info, NOT_BUILT);
+            }
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: the project has not been built");
+            }
+            return;
+        }
+        notBuiltNoticeShown.set(false);
+        if (!fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.info("Compile skipped, " + path + " has syntax errors");
+            return;
+        }
+        if (!buildInFlight.compareAndSet(false, true)) {
+            pending.accumulateAndGet(new PendingBuild(path, text, fullScope),
+                    (old, next) -> old != null && old.fullScope && !next.fullScope
+                            ? new PendingBuild(next.path, next.text, true)
+                            : next);
+            drainPendingIfUnclaimed();
+            return;
+        }
+        submit(path, text, fullScope);
+    }
+
+    private void drainPendingIfUnclaimed() {
+        while (pending.get() != null) {
+            if (!buildInFlight.compareAndSet(false, true)) {
+                return;
+            }
+            PendingBuild queued = pending.getAndSet(null);
+            if (queued != null) {
+                submit(queued.path, queued.text, queued.fullScope);
+                return;
+            }
+            buildInFlight.set(false);
+        }
+    }
+
+    private void submit(Path path, String text, boolean fullScope) {
+        buildExecutor.submit(() -> {
+            Engine target = engine.get();
+            boolean updated = false;
+            boolean ran = false;
+            boolean noEngine = false;
+            int errors = 0;
+            int warnings = 0;
+            Compilation result = null;
+            try {
+                if (target == null || !target.available()) {
+                    noEngine = true;
+                    logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+                    return;
+                }
+                if (fullScope) {
+                    server.showMessage(MessageType.Info, "Compiling every DRL file in the workspace");
+                }
+                result = compile(normalize(path), text, fullScope, target);
+                updated = commit(result);
+                errors = result.count(DiagnosticSeverity.Error);
+                warnings = result.count(DiagnosticSeverity.Warning);
+                ran = true;
+            } catch (Throwable t) {
+                logger.log(Level.WARNING, "Drools compile failed for " + path, t);
+                result = failure(normalize(path), text, t);
+                updated = commit(result);
+                errors = 1;
+                ran = true;
+            } finally {
+                if (updated) {
+                    server.refreshDiagnostics();
+                }
+                if (fullScope) {
+                    if (noEngine) {
+                        server.showMessage(MessageType.Warning,
+                                "Workspace compile skipped: no Drools engine on the project classpath");
+                    } else if (ran) {
+                        server.showMessage(errors > 0 ? MessageType.Warning : MessageType.Info,
+                                summaryMessage(errors, warnings, result));
+                    }
+                }
+                buildInFlight.set(false);
+                PendingBuild queued = pending.getAndSet(null);
+                if (queued != null) {
+                    trigger(queued.path, queued.text, queued.fullScope);
+                }
+            }
+        });
+    }
+
+    private static String summaryMessage(int errors, int warnings, Compilation result) {
+        String base = "Workspace compile finished: " + errors + " error(s), " + warnings + " warning(s)";
+        if (errors == 0 || result == null) {
+            return base;
+        }
+        return base + " in " + joinFileNames(result.filesWithErrors());
+    }
+
+    private static String joinFileNames(List<String> files) {
+        StringBuilder names = new StringBuilder();
+        int shown = Math.min(3, files.size());
+        for (int i = 0; i < shown; i++) {
+            if (i > 0) {
+                names.append(", ");
+            }
+            names.append(files.get(i));
+        }
+        if (files.size() > 3) {
+            names.append(" and ").append(files.size() - 3).append(" more");
+        }
+        return names.toString();
+    }
+
+    private Compilation compile(Path saved, String text, boolean fullScope, Engine target) throws Exception {
+        Path root = rootFor(saved);
+        Map<Path, String> sources = new LinkedHashMap<>();
+        Set<Path> skipped = new LinkedHashSet<>();
+        for (Path sibling : scope(saved, fullScope)) {
+            Path normalized = normalize(sibling);
+            if (normalized.equals(saved)) {
+                continue;
+            }
+            String siblingText = openText.apply(normalized);
+            if (siblingText == null) {
+                siblingText = readQuietly(normalized);
+            }
+            if (siblingText == null) {
+                skipped.add(normalized);
+                continue;
+            }
+            if (hasErrors(DRLDiagnosticHelper.parse(siblingText).diagnostics)) {
+                logger.fine(() -> "Leaving " + normalized + " out of the compile, it has syntax errors");
+                skipped.add(normalized);
+                continue;
+            }
+            sources.put(normalized, siblingText);
+        }
+        if (fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.fine(() -> "Leaving " + saved + " out of the compile, it has syntax errors");
+            skipped.add(saved);
+        } else {
+            sources.put(saved, text);

Review Comment:
   A full rebuild always adds the active file even when `workspaceDrlFiles()` deliberately omitted it. Opening a DRL excluded by `files.exclude` and running the command therefore compiles that excluded file, despite the rebuild being defined by the client-enumerated file set. Add the saved file only for save-scoped builds or when it is present in the authoritative full scope.



##########
packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/CompileDiagnostics.java:
##########
@@ -0,0 +1,645 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.lsp.server;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import org.drools.completion.DRLDiagnosticHelper;
+import org.drools.completion.WorkspaceScan;
+import org.drools.completion.WorkspaceSiblingResolvers;
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.DiagnosticSeverity;
+import org.eclipse.lsp4j.MessageType;
+import org.eclipse.lsp4j.Range;
+
+/**
+ * Compiles a saved DRL file together with its group through the project's own
+ * engine and keeps the engine's messages per file until the file is edited,
+ * saved again or regrouped. One compile runs at a time; a save during a compile
+ * queues one more and the latest wins.
+ */
+final class CompileDiagnostics {
+
+    static final String SOURCE = "drools";
+    static final String KFS_PREFIX = "src/main/resources/";
+    static final int MAX_DIAGNOSTICS_PER_FILE = 500;
+    static final String NOT_BUILT =
+            "Compile diagnostics need the project's compiled classes. Build the project, then save again.";
+
+    private static final Pattern NOISY_CASCADE = Pattern.compile(
+            "(?i)(unknown\\s*type|cannot\\s*resolve\\s*type|unresolved\\s*type|unable\\s*to\\s*resolve\\s*type|class\\s*not\\s*found)");
+    private static final Logger logger = Logger.getLogger(CompileDiagnostics.class.getName());
+
+    private final DroolsLspServer server;
+    private final Function<Path, String> openText;
+    private final Map<Path, List<Diagnostic>> cached = new ConcurrentHashMap<>();
+    private final AtomicBoolean buildInFlight = new AtomicBoolean(false);
+    private final AtomicReference<PendingBuild> pending = new AtomicReference<>();
+    private final ExecutorService gateExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile-gate"));
+    private final ExecutorService buildExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile"));
+    private volatile boolean onSave = true;
+    private final AtomicReference<Engine> engine = new AtomicReference<>();
+    private final AtomicBoolean notBuiltNoticeShown = new AtomicBoolean(false);
+
+    CompileDiagnostics(DroolsLspServer server, Function<Path, String> openText) {
+        this.server = server;
+        this.openText = openText;
+    }
+
+    boolean onSave() {
+        return onSave;
+    }
+
+    void setOnSave(boolean onSave) {
+        this.onSave = onSave;
+    }
+
+    Engine engine() {
+        return engine.get();
+    }
+
+    /** Installs the engine; the previous one is closed once the build in flight, if any, has finished. */
+    synchronized void setEngine(Engine next) {
+        if (buildExecutor.isShutdown()) {
+            next.close();
+            return;
+        }
+        Engine previous = engine.getAndSet(next);
+        if (previous == null || previous.available() != next.available()) {
+            logger.info(next.available()
+                    ? "Drools engine found on the project classpath; compile diagnostics are on"
+                    : "No usable Drools engine on the project classpath (needs drools-compiler and drools-mvel); "
+                            + "compile diagnostics are off");
+        }
+        if (previous != null && previous != next) {
+            buildExecutor.submit(previous::close);
+        }
+    }
+
+    /** Releases the engine and stops both executors; the close runs after any build in flight. */
+    synchronized void shutdown() {
+        Engine current = engine.getAndSet(null);
+        if (current != null) {
+            buildExecutor.submit(current::close);
+        }
+        gateExecutor.shutdown();
+        buildExecutor.shutdown();
+    }
+
+    List<Diagnostic> cachedFor(Path path) {
+        return path == null ? List.of() : cached.getOrDefault(normalize(path), List.of());
+    }
+
+    void invalidate(Path path) {
+        if (path != null) {
+            cached.remove(normalize(path));
+        }
+    }
+
+    void invalidateAll() {
+        cached.clear();
+    }
+
+    void onDidSave(Path path, String text) {
+        if (!onSave || path == null || text == null) {
+            return;
+        }
+        trigger(path, text, false);
+    }
+
+    void rebuildWorkspace(Path path) {
+        if (path == null) {
+            return;
+        }
+        String text = openText.apply(normalize(path));
+        if (text == null) {
+            text = readQuietly(path);
+        }
+        if (text == null) {
+            server.showMessage(MessageType.Warning, "Workspace compile skipped: cannot read " + path);
+            return;
+        }
+        trigger(path, text, true);
+    }
+
+    private void trigger(Path path, String text, boolean fullScope) {
+        gateExecutor.submit(() -> triggerNow(path, text, fullScope));
+    }
+
+    void triggerNow(Path path, String text, boolean fullScope) {
+        Engine current = engine.get();
+        if (current == null || !current.available()) {
+            logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: no Drools engine on the project classpath");
+            }
+            return;
+        }
+        if (server.projectClassesMissing()) {
+            if (notBuiltNoticeShown.compareAndSet(false, true)) {
+                logger.info(NOT_BUILT);
+                server.showMessage(MessageType.Info, NOT_BUILT);
+            }
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: the project has not been built");
+            }
+            return;
+        }
+        notBuiltNoticeShown.set(false);
+        if (!fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.info("Compile skipped, " + path + " has syntax errors");
+            return;
+        }
+        if (!buildInFlight.compareAndSet(false, true)) {
+            pending.accumulateAndGet(new PendingBuild(path, text, fullScope),
+                    (old, next) -> old != null && old.fullScope && !next.fullScope
+                            ? new PendingBuild(next.path, next.text, true)
+                            : next);
+            drainPendingIfUnclaimed();
+            return;
+        }
+        submit(path, text, fullScope);
+    }
+
+    private void drainPendingIfUnclaimed() {
+        while (pending.get() != null) {
+            if (!buildInFlight.compareAndSet(false, true)) {
+                return;
+            }
+            PendingBuild queued = pending.getAndSet(null);
+            if (queued != null) {
+                submit(queued.path, queued.text, queued.fullScope);
+                return;
+            }
+            buildInFlight.set(false);
+        }
+    }
+
+    private void submit(Path path, String text, boolean fullScope) {
+        buildExecutor.submit(() -> {
+            Engine target = engine.get();
+            boolean updated = false;
+            boolean ran = false;
+            boolean noEngine = false;
+            int errors = 0;
+            int warnings = 0;
+            Compilation result = null;
+            try {
+                if (target == null || !target.available()) {
+                    noEngine = true;
+                    logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+                    return;
+                }
+                if (fullScope) {
+                    server.showMessage(MessageType.Info, "Compiling every DRL file in the workspace");
+                }
+                result = compile(normalize(path), text, fullScope, target);
+                updated = commit(result);
+                errors = result.count(DiagnosticSeverity.Error);
+                warnings = result.count(DiagnosticSeverity.Warning);
+                ran = true;
+            } catch (Throwable t) {
+                logger.log(Level.WARNING, "Drools compile failed for " + path, t);
+                result = failure(normalize(path), text, t);
+                updated = commit(result);
+                errors = 1;
+                ran = true;
+            } finally {
+                if (updated) {
+                    server.refreshDiagnostics();
+                }
+                if (fullScope) {
+                    if (noEngine) {
+                        server.showMessage(MessageType.Warning,
+                                "Workspace compile skipped: no Drools engine on the project classpath");
+                    } else if (ran) {
+                        server.showMessage(errors > 0 ? MessageType.Warning : MessageType.Info,
+                                summaryMessage(errors, warnings, result));
+                    }
+                }
+                buildInFlight.set(false);
+                PendingBuild queued = pending.getAndSet(null);
+                if (queued != null) {
+                    trigger(queued.path, queued.text, queued.fullScope);
+                }
+            }
+        });
+    }
+
+    private static String summaryMessage(int errors, int warnings, Compilation result) {
+        String base = "Workspace compile finished: " + errors + " error(s), " + warnings + " warning(s)";
+        if (errors == 0 || result == null) {
+            return base;
+        }
+        return base + " in " + joinFileNames(result.filesWithErrors());
+    }
+
+    private static String joinFileNames(List<String> files) {
+        StringBuilder names = new StringBuilder();
+        int shown = Math.min(3, files.size());
+        for (int i = 0; i < shown; i++) {
+            if (i > 0) {
+                names.append(", ");
+            }
+            names.append(files.get(i));
+        }
+        if (files.size() > 3) {
+            names.append(" and ").append(files.size() - 3).append(" more");
+        }
+        return names.toString();
+    }
+
+    private Compilation compile(Path saved, String text, boolean fullScope, Engine target) throws Exception {
+        Path root = rootFor(saved);
+        Map<Path, String> sources = new LinkedHashMap<>();
+        Set<Path> skipped = new LinkedHashSet<>();
+        for (Path sibling : scope(saved, fullScope)) {
+            Path normalized = normalize(sibling);
+            if (normalized.equals(saved)) {
+                continue;
+            }
+            String siblingText = openText.apply(normalized);
+            if (siblingText == null) {
+                siblingText = readQuietly(normalized);
+            }
+            if (siblingText == null) {
+                skipped.add(normalized);
+                continue;
+            }
+            if (hasErrors(DRLDiagnosticHelper.parse(siblingText).diagnostics)) {
+                logger.fine(() -> "Leaving " + normalized + " out of the compile, it has syntax errors");
+                skipped.add(normalized);
+                continue;
+            }
+            sources.put(normalized, siblingText);
+        }
+        if (fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.fine(() -> "Leaving " + saved + " out of the compile, it has syntax errors");
+            skipped.add(saved);
+        } else {
+            sources.put(saved, text);
+        }
+
+        Map<String, Path> fileByKfsPath = new LinkedHashMap<>();
+        Map<String, String> drlByKfsPath = new LinkedHashMap<>();
+        for (Map.Entry<Path, String> entry : sources.entrySet()) {
+            String kfsPath = kfsPathFor(root, entry.getKey(), fileByKfsPath.size());
+            fileByKfsPath.put(kfsPath, entry.getKey());
+            drlByKfsPath.put(kfsPath, entry.getValue());
+        }
+        logger.info(() -> "Compiling " + sources.size() + " DRL file(s) for " + saved
+                + (fullScope ? " (whole workspace)" : ""));
+
+        List<Map<String, Object>> messages = target.build(drlByKfsPath);
+
+        Map<Path, List<Diagnostic>> perFile = new LinkedHashMap<>();
+        for (Path file : sources.keySet()) {
+            perFile.put(file, new ArrayList<>());
+        }
+        Path fallback = sources.containsKey(saved) ? saved : sources.keySet().stream().findFirst().orElse(null);
+        if (fallback != null) {
+            for (Map<String, Object> m : messages) {
+                Path file = fileFor(m.get("path"), fileByKfsPath, fallback);
+                List<Diagnostic> bucket = perFile.get(file);
+                if (bucket == null) {
+                    file = fallback;
+                    bucket = perFile.get(fallback);
+                }
+                String messageText = m.get("text") == null ? "" : String.valueOf(m.get("text"));
+                Diagnostic d = new Diagnostic();
+                d.setSeverity(severityOf(m.get("level")));
+                d.setSource(SOURCE);
+                d.setMessage(messageText);
+                d.setRange(BuildMessageRanges.rangeFor(messageText, intOf(m.get("line")), intOf(m.get("column")),
+                        sources.get(file)));
+                bucket.add(d);
+            }
+        }
+        perFile.replaceAll((file, list) -> cap(dedupe(dropCascades(list))));
+        return new Compilation(sources, perFile, skipped);
+    }
+
+    private Compilation failure(Path current, String text, Throwable t) {
+        Diagnostic d = new Diagnostic();
+        d.setSource(SOURCE);
+        String missing = missingClass(t);
+        if (missing != null) {
+            d.setSeverity(DiagnosticSeverity.Warning);
+            d.setMessage("The Drools compile needs the project's compiled classes: " + missing
+                    + " is not on the classpath. Build the project, then save again.");
+        } else {
+            d.setSeverity(DiagnosticSeverity.Error);
+            d.setMessage("Drools compile failed: " + summarize(t));
+        }
+        d.setRange(BuildMessageRanges.firstRuleRange(text));
+        Map<Path, List<Diagnostic>> perFile = new LinkedHashMap<>();
+        perFile.put(current, List.of(d));
+        return new Compilation(Map.of(current, text), perFile, Set.of());
+    }
+
+    private boolean commit(Compilation result) {
+        boolean[] updated = {false};
+        result.perFile.forEach((file, list) -> {
+            String compiled = result.sources.get(file);
+            cached.compute(file, (k, previous) -> {
+                String now = openText.apply(file);
+                if (now != null && !now.equals(compiled)) {
+                    return previous;
+                }
+                updated[0] = true;
+                return list.isEmpty() ? null : List.copyOf(list);

Review Comment:
   A cache invalidation can be undone by an already-running compile. After `didClose`, `openText.apply(file)` returns `null`, so this branch accepts and re-caches the old result; reopening the file can then show stale diagnostics. The same race lets an old-group build repopulate the cache after `invalidateAll()`. Capture an invalidation/generation token when the build starts and reject commits whose per-file or global generation changed.



##########
packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/CompileDiagnostics.java:
##########
@@ -0,0 +1,645 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.lsp.server;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import org.drools.completion.DRLDiagnosticHelper;
+import org.drools.completion.WorkspaceScan;
+import org.drools.completion.WorkspaceSiblingResolvers;
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.DiagnosticSeverity;
+import org.eclipse.lsp4j.MessageType;
+import org.eclipse.lsp4j.Range;
+
+/**
+ * Compiles a saved DRL file together with its group through the project's own
+ * engine and keeps the engine's messages per file until the file is edited,
+ * saved again or regrouped. One compile runs at a time; a save during a compile
+ * queues one more and the latest wins.
+ */
+final class CompileDiagnostics {
+
+    static final String SOURCE = "drools";
+    static final String KFS_PREFIX = "src/main/resources/";
+    static final int MAX_DIAGNOSTICS_PER_FILE = 500;
+    static final String NOT_BUILT =
+            "Compile diagnostics need the project's compiled classes. Build the project, then save again.";
+
+    private static final Pattern NOISY_CASCADE = Pattern.compile(
+            "(?i)(unknown\\s*type|cannot\\s*resolve\\s*type|unresolved\\s*type|unable\\s*to\\s*resolve\\s*type|class\\s*not\\s*found)");
+    private static final Logger logger = Logger.getLogger(CompileDiagnostics.class.getName());
+
+    private final DroolsLspServer server;
+    private final Function<Path, String> openText;
+    private final Map<Path, List<Diagnostic>> cached = new ConcurrentHashMap<>();
+    private final AtomicBoolean buildInFlight = new AtomicBoolean(false);
+    private final AtomicReference<PendingBuild> pending = new AtomicReference<>();
+    private final ExecutorService gateExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile-gate"));
+    private final ExecutorService buildExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile"));
+    private volatile boolean onSave = true;
+    private final AtomicReference<Engine> engine = new AtomicReference<>();
+    private final AtomicBoolean notBuiltNoticeShown = new AtomicBoolean(false);
+
+    CompileDiagnostics(DroolsLspServer server, Function<Path, String> openText) {
+        this.server = server;
+        this.openText = openText;
+    }
+
+    boolean onSave() {
+        return onSave;
+    }
+
+    void setOnSave(boolean onSave) {
+        this.onSave = onSave;
+    }
+
+    Engine engine() {
+        return engine.get();
+    }
+
+    /** Installs the engine; the previous one is closed once the build in flight, if any, has finished. */
+    synchronized void setEngine(Engine next) {
+        if (buildExecutor.isShutdown()) {
+            next.close();
+            return;
+        }
+        Engine previous = engine.getAndSet(next);
+        if (previous == null || previous.available() != next.available()) {
+            logger.info(next.available()
+                    ? "Drools engine found on the project classpath; compile diagnostics are on"
+                    : "No usable Drools engine on the project classpath (needs drools-compiler and drools-mvel); "
+                            + "compile diagnostics are off");
+        }
+        if (previous != null && previous != next) {
+            buildExecutor.submit(previous::close);
+        }
+    }
+
+    /** Releases the engine and stops both executors; the close runs after any build in flight. */
+    synchronized void shutdown() {
+        Engine current = engine.getAndSet(null);
+        if (current != null) {
+            buildExecutor.submit(current::close);
+        }
+        gateExecutor.shutdown();
+        buildExecutor.shutdown();
+    }
+
+    List<Diagnostic> cachedFor(Path path) {
+        return path == null ? List.of() : cached.getOrDefault(normalize(path), List.of());
+    }
+
+    void invalidate(Path path) {
+        if (path != null) {
+            cached.remove(normalize(path));
+        }
+    }
+
+    void invalidateAll() {
+        cached.clear();
+    }
+
+    void onDidSave(Path path, String text) {
+        if (!onSave || path == null || text == null) {
+            return;
+        }
+        trigger(path, text, false);
+    }
+
+    void rebuildWorkspace(Path path) {
+        if (path == null) {
+            return;
+        }
+        String text = openText.apply(normalize(path));
+        if (text == null) {
+            text = readQuietly(path);
+        }
+        if (text == null) {
+            server.showMessage(MessageType.Warning, "Workspace compile skipped: cannot read " + path);
+            return;
+        }
+        trigger(path, text, true);
+    }
+
+    private void trigger(Path path, String text, boolean fullScope) {
+        gateExecutor.submit(() -> triggerNow(path, text, fullScope));
+    }
+
+    void triggerNow(Path path, String text, boolean fullScope) {
+        Engine current = engine.get();
+        if (current == null || !current.available()) {
+            logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: no Drools engine on the project classpath");
+            }
+            return;
+        }
+        if (server.projectClassesMissing()) {
+            if (notBuiltNoticeShown.compareAndSet(false, true)) {
+                logger.info(NOT_BUILT);
+                server.showMessage(MessageType.Info, NOT_BUILT);
+            }
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: the project has not been built");
+            }
+            return;
+        }
+        notBuiltNoticeShown.set(false);
+        if (!fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.info("Compile skipped, " + path + " has syntax errors");
+            return;
+        }
+        if (!buildInFlight.compareAndSet(false, true)) {
+            pending.accumulateAndGet(new PendingBuild(path, text, fullScope),
+                    (old, next) -> old != null && old.fullScope && !next.fullScope
+                            ? new PendingBuild(next.path, next.text, true)
+                            : next);
+            drainPendingIfUnclaimed();
+            return;
+        }
+        submit(path, text, fullScope);
+    }
+
+    private void drainPendingIfUnclaimed() {
+        while (pending.get() != null) {
+            if (!buildInFlight.compareAndSet(false, true)) {
+                return;
+            }
+            PendingBuild queued = pending.getAndSet(null);
+            if (queued != null) {
+                submit(queued.path, queued.text, queued.fullScope);
+                return;
+            }
+            buildInFlight.set(false);
+        }
+    }
+
+    private void submit(Path path, String text, boolean fullScope) {
+        buildExecutor.submit(() -> {
+            Engine target = engine.get();
+            boolean updated = false;
+            boolean ran = false;
+            boolean noEngine = false;
+            int errors = 0;
+            int warnings = 0;
+            Compilation result = null;
+            try {
+                if (target == null || !target.available()) {
+                    noEngine = true;
+                    logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+                    return;
+                }
+                if (fullScope) {
+                    server.showMessage(MessageType.Info, "Compiling every DRL file in the workspace");
+                }
+                result = compile(normalize(path), text, fullScope, target);
+                updated = commit(result);
+                errors = result.count(DiagnosticSeverity.Error);
+                warnings = result.count(DiagnosticSeverity.Warning);
+                ran = true;
+            } catch (Throwable t) {
+                logger.log(Level.WARNING, "Drools compile failed for " + path, t);
+                result = failure(normalize(path), text, t);
+                updated = commit(result);
+                errors = 1;
+                ran = true;
+            } finally {
+                if (updated) {
+                    server.refreshDiagnostics();
+                }
+                if (fullScope) {
+                    if (noEngine) {
+                        server.showMessage(MessageType.Warning,
+                                "Workspace compile skipped: no Drools engine on the project classpath");
+                    } else if (ran) {
+                        server.showMessage(errors > 0 ? MessageType.Warning : MessageType.Info,
+                                summaryMessage(errors, warnings, result));
+                    }
+                }
+                buildInFlight.set(false);
+                PendingBuild queued = pending.getAndSet(null);
+                if (queued != null) {
+                    trigger(queued.path, queued.text, queued.fullScope);
+                }
+            }
+        });
+    }
+
+    private static String summaryMessage(int errors, int warnings, Compilation result) {
+        String base = "Workspace compile finished: " + errors + " error(s), " + warnings + " warning(s)";
+        if (errors == 0 || result == null) {
+            return base;
+        }
+        return base + " in " + joinFileNames(result.filesWithErrors());
+    }
+
+    private static String joinFileNames(List<String> files) {
+        StringBuilder names = new StringBuilder();
+        int shown = Math.min(3, files.size());
+        for (int i = 0; i < shown; i++) {
+            if (i > 0) {
+                names.append(", ");
+            }
+            names.append(files.get(i));
+        }
+        if (files.size() > 3) {
+            names.append(" and ").append(files.size() - 3).append(" more");
+        }
+        return names.toString();
+    }
+
+    private Compilation compile(Path saved, String text, boolean fullScope, Engine target) throws Exception {
+        Path root = rootFor(saved);
+        Map<Path, String> sources = new LinkedHashMap<>();
+        Set<Path> skipped = new LinkedHashSet<>();
+        for (Path sibling : scope(saved, fullScope)) {
+            Path normalized = normalize(sibling);
+            if (normalized.equals(saved)) {
+                continue;
+            }
+            String siblingText = openText.apply(normalized);
+            if (siblingText == null) {
+                siblingText = readQuietly(normalized);
+            }
+            if (siblingText == null) {
+                skipped.add(normalized);
+                continue;
+            }
+            if (hasErrors(DRLDiagnosticHelper.parse(siblingText).diagnostics)) {
+                logger.fine(() -> "Leaving " + normalized + " out of the compile, it has syntax errors");
+                skipped.add(normalized);
+                continue;
+            }
+            sources.put(normalized, siblingText);
+        }
+        if (fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.fine(() -> "Leaving " + saved + " out of the compile, it has syntax errors");
+            skipped.add(saved);
+        } else {
+            sources.put(saved, text);
+        }
+
+        Map<String, Path> fileByKfsPath = new LinkedHashMap<>();
+        Map<String, String> drlByKfsPath = new LinkedHashMap<>();
+        for (Map.Entry<Path, String> entry : sources.entrySet()) {
+            String kfsPath = kfsPathFor(root, entry.getKey(), fileByKfsPath.size());
+            fileByKfsPath.put(kfsPath, entry.getKey());
+            drlByKfsPath.put(kfsPath, entry.getValue());
+        }
+        logger.info(() -> "Compiling " + sources.size() + " DRL file(s) for " + saved
+                + (fullScope ? " (whole workspace)" : ""));
+
+        List<Map<String, Object>> messages = target.build(drlByKfsPath);
+
+        Map<Path, List<Diagnostic>> perFile = new LinkedHashMap<>();
+        for (Path file : sources.keySet()) {
+            perFile.put(file, new ArrayList<>());
+        }
+        Path fallback = sources.containsKey(saved) ? saved : sources.keySet().stream().findFirst().orElse(null);
+        if (fallback != null) {
+            for (Map<String, Object> m : messages) {
+                Path file = fileFor(m.get("path"), fileByKfsPath, fallback);
+                List<Diagnostic> bucket = perFile.get(file);
+                if (bucket == null) {
+                    file = fallback;
+                    bucket = perFile.get(fallback);
+                }
+                String messageText = m.get("text") == null ? "" : String.valueOf(m.get("text"));
+                Diagnostic d = new Diagnostic();
+                d.setSeverity(severityOf(m.get("level")));
+                d.setSource(SOURCE);
+                d.setMessage(messageText);
+                d.setRange(BuildMessageRanges.rangeFor(messageText, intOf(m.get("line")), intOf(m.get("column")),
+                        sources.get(file)));
+                bucket.add(d);
+            }
+        }
+        perFile.replaceAll((file, list) -> cap(dedupe(dropCascades(list))));
+        return new Compilation(sources, perFile, skipped);
+    }
+
+    private Compilation failure(Path current, String text, Throwable t) {
+        Diagnostic d = new Diagnostic();
+        d.setSource(SOURCE);
+        String missing = missingClass(t);
+        if (missing != null) {
+            d.setSeverity(DiagnosticSeverity.Warning);
+            d.setMessage("The Drools compile needs the project's compiled classes: " + missing
+                    + " is not on the classpath. Build the project, then save again.");
+        } else {
+            d.setSeverity(DiagnosticSeverity.Error);
+            d.setMessage("Drools compile failed: " + summarize(t));
+        }
+        d.setRange(BuildMessageRanges.firstRuleRange(text));
+        Map<Path, List<Diagnostic>> perFile = new LinkedHashMap<>();
+        perFile.put(current, List.of(d));
+        return new Compilation(Map.of(current, text), perFile, Set.of());
+    }
+
+    private boolean commit(Compilation result) {
+        boolean[] updated = {false};
+        result.perFile.forEach((file, list) -> {
+            String compiled = result.sources.get(file);
+            cached.compute(file, (k, previous) -> {
+                String now = openText.apply(file);
+                if (now != null && !now.equals(compiled)) {
+                    return previous;
+                }
+                updated[0] = true;
+                return list.isEmpty() ? null : List.copyOf(list);
+            });
+        });
+        for (Path file : result.skipped()) {
+            if (cached.remove(file) != null) {
+                updated[0] = true;
+            }
+        }
+        return updated[0];
+    }
+
+    private List<Path> scope(Path current, boolean fullScope) {
+        List<Path> files;
+        if (!fullScope) {
+            files = WorkspaceSiblingResolvers.active().resolveSiblings(current);
+        } else {
+            files = WorkspaceSiblingResolvers.active().workspaceDrlFiles();
+            if (files.isEmpty()) {
+                files = WorkspaceScan.of(rootFor(current)).drlFiles();

Review Comment:
   An empty list is also the valid authoritative result when VS Code enumerates no DRL files (for example, all are excluded). Falling back to a disk walk then reintroduces files filtered by `files.exclude`/ignore rules, contrary to the rebuild contract. Preserve whether the resolver has an authoritative workspace view separately from the list's emptiness.



##########
packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/CompileDiagnostics.java:
##########
@@ -0,0 +1,645 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.lsp.server;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import org.drools.completion.DRLDiagnosticHelper;
+import org.drools.completion.WorkspaceScan;
+import org.drools.completion.WorkspaceSiblingResolvers;
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.DiagnosticSeverity;
+import org.eclipse.lsp4j.MessageType;
+import org.eclipse.lsp4j.Range;
+
+/**
+ * Compiles a saved DRL file together with its group through the project's own
+ * engine and keeps the engine's messages per file until the file is edited,
+ * saved again or regrouped. One compile runs at a time; a save during a compile
+ * queues one more and the latest wins.
+ */
+final class CompileDiagnostics {
+
+    static final String SOURCE = "drools";
+    static final String KFS_PREFIX = "src/main/resources/";
+    static final int MAX_DIAGNOSTICS_PER_FILE = 500;
+    static final String NOT_BUILT =
+            "Compile diagnostics need the project's compiled classes. Build the project, then save again.";
+
+    private static final Pattern NOISY_CASCADE = Pattern.compile(
+            "(?i)(unknown\\s*type|cannot\\s*resolve\\s*type|unresolved\\s*type|unable\\s*to\\s*resolve\\s*type|class\\s*not\\s*found)");
+    private static final Logger logger = Logger.getLogger(CompileDiagnostics.class.getName());
+
+    private final DroolsLspServer server;
+    private final Function<Path, String> openText;
+    private final Map<Path, List<Diagnostic>> cached = new ConcurrentHashMap<>();
+    private final AtomicBoolean buildInFlight = new AtomicBoolean(false);
+    private final AtomicReference<PendingBuild> pending = new AtomicReference<>();
+    private final ExecutorService gateExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile-gate"));
+    private final ExecutorService buildExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile"));
+    private volatile boolean onSave = true;
+    private final AtomicReference<Engine> engine = new AtomicReference<>();
+    private final AtomicBoolean notBuiltNoticeShown = new AtomicBoolean(false);
+
+    CompileDiagnostics(DroolsLspServer server, Function<Path, String> openText) {
+        this.server = server;
+        this.openText = openText;
+    }
+
+    boolean onSave() {
+        return onSave;
+    }
+
+    void setOnSave(boolean onSave) {
+        this.onSave = onSave;
+    }
+
+    Engine engine() {
+        return engine.get();
+    }
+
+    /** Installs the engine; the previous one is closed once the build in flight, if any, has finished. */
+    synchronized void setEngine(Engine next) {
+        if (buildExecutor.isShutdown()) {
+            next.close();
+            return;
+        }
+        Engine previous = engine.getAndSet(next);
+        if (previous == null || previous.available() != next.available()) {
+            logger.info(next.available()
+                    ? "Drools engine found on the project classpath; compile diagnostics are on"
+                    : "No usable Drools engine on the project classpath (needs drools-compiler and drools-mvel); "
+                            + "compile diagnostics are off");
+        }
+        if (previous != null && previous != next) {
+            buildExecutor.submit(previous::close);
+        }
+    }
+
+    /** Releases the engine and stops both executors; the close runs after any build in flight. */
+    synchronized void shutdown() {
+        Engine current = engine.getAndSet(null);
+        if (current != null) {
+            buildExecutor.submit(current::close);
+        }
+        gateExecutor.shutdown();
+        buildExecutor.shutdown();
+    }
+
+    List<Diagnostic> cachedFor(Path path) {
+        return path == null ? List.of() : cached.getOrDefault(normalize(path), List.of());
+    }
+
+    void invalidate(Path path) {
+        if (path != null) {
+            cached.remove(normalize(path));
+        }
+    }
+
+    void invalidateAll() {
+        cached.clear();
+    }
+
+    void onDidSave(Path path, String text) {
+        if (!onSave || path == null || text == null) {
+            return;
+        }
+        trigger(path, text, false);
+    }
+
+    void rebuildWorkspace(Path path) {
+        if (path == null) {
+            return;
+        }
+        String text = openText.apply(normalize(path));
+        if (text == null) {
+            text = readQuietly(path);
+        }
+        if (text == null) {
+            server.showMessage(MessageType.Warning, "Workspace compile skipped: cannot read " + path);
+            return;
+        }
+        trigger(path, text, true);
+    }
+
+    private void trigger(Path path, String text, boolean fullScope) {
+        gateExecutor.submit(() -> triggerNow(path, text, fullScope));
+    }
+
+    void triggerNow(Path path, String text, boolean fullScope) {
+        Engine current = engine.get();
+        if (current == null || !current.available()) {
+            logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: no Drools engine on the project classpath");
+            }
+            return;
+        }
+        if (server.projectClassesMissing()) {
+            if (notBuiltNoticeShown.compareAndSet(false, true)) {
+                logger.info(NOT_BUILT);
+                server.showMessage(MessageType.Info, NOT_BUILT);
+            }
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: the project has not been built");
+            }
+            return;
+        }
+        notBuiltNoticeShown.set(false);
+        if (!fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.info("Compile skipped, " + path + " has syntax errors");
+            return;
+        }
+        if (!buildInFlight.compareAndSet(false, true)) {
+            pending.accumulateAndGet(new PendingBuild(path, text, fullScope),
+                    (old, next) -> old != null && old.fullScope && !next.fullScope
+                            ? new PendingBuild(next.path, next.text, true)
+                            : next);
+            drainPendingIfUnclaimed();
+            return;
+        }
+        submit(path, text, fullScope);
+    }
+
+    private void drainPendingIfUnclaimed() {
+        while (pending.get() != null) {
+            if (!buildInFlight.compareAndSet(false, true)) {
+                return;
+            }
+            PendingBuild queued = pending.getAndSet(null);
+            if (queued != null) {
+                submit(queued.path, queued.text, queued.fullScope);
+                return;
+            }
+            buildInFlight.set(false);
+        }
+    }
+
+    private void submit(Path path, String text, boolean fullScope) {
+        buildExecutor.submit(() -> {
+            Engine target = engine.get();
+            boolean updated = false;
+            boolean ran = false;
+            boolean noEngine = false;
+            int errors = 0;
+            int warnings = 0;
+            Compilation result = null;
+            try {
+                if (target == null || !target.available()) {
+                    noEngine = true;
+                    logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+                    return;
+                }
+                if (fullScope) {
+                    server.showMessage(MessageType.Info, "Compiling every DRL file in the workspace");
+                }
+                result = compile(normalize(path), text, fullScope, target);
+                updated = commit(result);
+                errors = result.count(DiagnosticSeverity.Error);
+                warnings = result.count(DiagnosticSeverity.Warning);
+                ran = true;
+            } catch (Throwable t) {
+                logger.log(Level.WARNING, "Drools compile failed for " + path, t);
+                result = failure(normalize(path), text, t);
+                updated = commit(result);
+                errors = 1;
+                ran = true;
+            } finally {
+                if (updated) {
+                    server.refreshDiagnostics();
+                }
+                if (fullScope) {
+                    if (noEngine) {
+                        server.showMessage(MessageType.Warning,
+                                "Workspace compile skipped: no Drools engine on the project classpath");
+                    } else if (ran) {
+                        server.showMessage(errors > 0 ? MessageType.Warning : MessageType.Info,
+                                summaryMessage(errors, warnings, result));
+                    }
+                }
+                buildInFlight.set(false);
+                PendingBuild queued = pending.getAndSet(null);
+                if (queued != null) {
+                    trigger(queued.path, queued.text, queued.fullScope);
+                }
+            }
+        });
+    }
+
+    private static String summaryMessage(int errors, int warnings, Compilation result) {
+        String base = "Workspace compile finished: " + errors + " error(s), " + warnings + " warning(s)";
+        if (errors == 0 || result == null) {
+            return base;
+        }
+        return base + " in " + joinFileNames(result.filesWithErrors());
+    }
+
+    private static String joinFileNames(List<String> files) {
+        StringBuilder names = new StringBuilder();
+        int shown = Math.min(3, files.size());
+        for (int i = 0; i < shown; i++) {
+            if (i > 0) {
+                names.append(", ");
+            }
+            names.append(files.get(i));
+        }
+        if (files.size() > 3) {
+            names.append(" and ").append(files.size() - 3).append(" more");
+        }
+        return names.toString();
+    }
+
+    private Compilation compile(Path saved, String text, boolean fullScope, Engine target) throws Exception {
+        Path root = rootFor(saved);
+        Map<Path, String> sources = new LinkedHashMap<>();
+        Set<Path> skipped = new LinkedHashSet<>();
+        for (Path sibling : scope(saved, fullScope)) {
+            Path normalized = normalize(sibling);
+            if (normalized.equals(saved)) {
+                continue;
+            }
+            String siblingText = openText.apply(normalized);
+            if (siblingText == null) {
+                siblingText = readQuietly(normalized);
+            }
+            if (siblingText == null) {
+                skipped.add(normalized);
+                continue;
+            }
+            if (hasErrors(DRLDiagnosticHelper.parse(siblingText).diagnostics)) {
+                logger.fine(() -> "Leaving " + normalized + " out of the compile, it has syntax errors");
+                skipped.add(normalized);
+                continue;
+            }
+            sources.put(normalized, siblingText);
+        }
+        if (fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.fine(() -> "Leaving " + saved + " out of the compile, it has syntax errors");
+            skipped.add(saved);
+        } else {
+            sources.put(saved, text);
+        }
+
+        Map<String, Path> fileByKfsPath = new LinkedHashMap<>();
+        Map<String, String> drlByKfsPath = new LinkedHashMap<>();
+        for (Map.Entry<Path, String> entry : sources.entrySet()) {
+            String kfsPath = kfsPathFor(root, entry.getKey(), fileByKfsPath.size());
+            fileByKfsPath.put(kfsPath, entry.getKey());
+            drlByKfsPath.put(kfsPath, entry.getValue());
+        }
+        logger.info(() -> "Compiling " + sources.size() + " DRL file(s) for " + saved
+                + (fullScope ? " (whole workspace)" : ""));
+
+        List<Map<String, Object>> messages = target.build(drlByKfsPath);
+
+        Map<Path, List<Diagnostic>> perFile = new LinkedHashMap<>();
+        for (Path file : sources.keySet()) {
+            perFile.put(file, new ArrayList<>());
+        }
+        Path fallback = sources.containsKey(saved) ? saved : sources.keySet().stream().findFirst().orElse(null);
+        if (fallback != null) {
+            for (Map<String, Object> m : messages) {
+                Path file = fileFor(m.get("path"), fileByKfsPath, fallback);
+                List<Diagnostic> bucket = perFile.get(file);
+                if (bucket == null) {
+                    file = fallback;
+                    bucket = perFile.get(fallback);
+                }
+                String messageText = m.get("text") == null ? "" : String.valueOf(m.get("text"));
+                Diagnostic d = new Diagnostic();
+                d.setSeverity(severityOf(m.get("level")));
+                d.setSource(SOURCE);
+                d.setMessage(messageText);
+                d.setRange(BuildMessageRanges.rangeFor(messageText, intOf(m.get("line")), intOf(m.get("column")),
+                        sources.get(file)));
+                bucket.add(d);
+            }
+        }
+        perFile.replaceAll((file, list) -> cap(dedupe(dropCascades(list))));
+        return new Compilation(sources, perFile, skipped);
+    }
+
+    private Compilation failure(Path current, String text, Throwable t) {
+        Diagnostic d = new Diagnostic();
+        d.setSource(SOURCE);
+        String missing = missingClass(t);
+        if (missing != null) {
+            d.setSeverity(DiagnosticSeverity.Warning);
+            d.setMessage("The Drools compile needs the project's compiled classes: " + missing
+                    + " is not on the classpath. Build the project, then save again.");
+        } else {
+            d.setSeverity(DiagnosticSeverity.Error);
+            d.setMessage("Drools compile failed: " + summarize(t));
+        }
+        d.setRange(BuildMessageRanges.firstRuleRange(text));
+        Map<Path, List<Diagnostic>> perFile = new LinkedHashMap<>();
+        perFile.put(current, List.of(d));
+        return new Compilation(Map.of(current, text), perFile, Set.of());
+    }
+
+    private boolean commit(Compilation result) {
+        boolean[] updated = {false};
+        result.perFile.forEach((file, list) -> {
+            String compiled = result.sources.get(file);
+            cached.compute(file, (k, previous) -> {
+                String now = openText.apply(file);
+                if (now != null && !now.equals(compiled)) {
+                    return previous;
+                }
+                updated[0] = true;
+                return list.isEmpty() ? null : List.copyOf(list);
+            });
+        });
+        for (Path file : result.skipped()) {
+            if (cached.remove(file) != null) {
+                updated[0] = true;
+            }
+        }
+        return updated[0];
+    }
+
+    private List<Path> scope(Path current, boolean fullScope) {
+        List<Path> files;
+        if (!fullScope) {
+            files = WorkspaceSiblingResolvers.active().resolveSiblings(current);
+        } else {
+            files = WorkspaceSiblingResolvers.active().workspaceDrlFiles();
+            if (files.isEmpty()) {
+                files = WorkspaceScan.of(rootFor(current)).drlFiles();
+            }
+        }
+        return withoutBuildOutput(files);
+    }
+
+    /** Build output holds copies of the rules; compiling them next to their sources only duplicates every rule. */
+    private List<Path> withoutBuildOutput(List<Path> files) {
+        List<Path> outputDirs = new ArrayList<>();
+        for (Path dir : server.getBuildOutputDirs()) {
+            outputDirs.add(normalize(dir));
+        }
+        if (outputDirs.isEmpty()) {
+            return files;
+        }
+        List<Path> kept = new ArrayList<>(files.size());
+        for (Path file : files) {
+            Path normalized = normalize(file);
+            if (outputDirs.stream().noneMatch(normalized::startsWith)) {
+                kept.add(file);
+            }
+        }
+        return kept;
+    }
+
+    private Path rootFor(Path current) {
+        Path root = server.workspaceRoot();
+        return root != null ? normalize(root) : current.getParent();
+    }
+
+    static String kfsPathFor(Path root, Path file, int index) {
+        Path abs = normalize(file);
+        if (root != null) {
+            Path r = normalize(root);
+            if (abs.startsWith(r)) {
+                return KFS_PREFIX + r.relativize(abs).toString().replace('\\', '/');
+            }
+        }
+        return KFS_PREFIX + "external/" + index + "/" + abs.getFileName();
+    }
+
+    private static Path fileFor(Object pathValue, Map<String, Path> fileByKfsPath, Path fallback) {
+        if (pathValue == null) {
+            return fallback;
+        }
+        String p = String.valueOf(pathValue).replace('\\', '/');
+        if (p.isBlank()) {
+            return fallback;
+        }
+        Path exact = fileByKfsPath.get(p);
+        if (exact != null) {
+            return exact;
+        }
+        for (Map.Entry<String, Path> entry : fileByKfsPath.entrySet()) {
+            String key = entry.getKey();
+            if (p.endsWith("/" + key) || key.endsWith("/" + p) || p.equals(key)) {
+                return entry.getValue();
+            }
+        }
+        return fallback;
+    }
+
+    static List<Diagnostic> merge(List<Diagnostic> fast, List<Diagnostic> compiled) {
+        if (compiled == null || compiled.isEmpty()) {
+            return fast == null ? List.of() : fast;
+        }
+        List<Diagnostic> merged = new ArrayList<>();
+        Set<String> seen = new HashSet<>();
+        if (fast != null) {
+            for (Diagnostic d : fast) {
+                merged.add(d);
+                seen.add(mergeKey(d));
+            }
+        }
+        for (Diagnostic d : compiled) {
+            if (seen.add(mergeKey(d))) {
+                merged.add(d);
+            }
+        }
+        return merged;
+    }
+
+    private static String mergeKey(Diagnostic d) {
+        Range r = d.getRange();
+        String where = r == null ? "" : r.getStart().getLine() + ":" + r.getStart().getCharacter() + "-"
+                + r.getEnd().getLine() + ":" + r.getEnd().getCharacter();
+        return where + "|" + d.getSeverity() + "|" + d.getMessage();
+    }
+
+    private static List<Diagnostic> dropCascades(List<Diagnostic> in) {
+        boolean hasRootError = in.stream().anyMatch(d -> d.getSeverity() == DiagnosticSeverity.Error
+                && (d.getMessage() == null || !NOISY_CASCADE.matcher(d.getMessage()).find()));
+        if (!hasRootError) {
+            return in;
+        }
+        List<Diagnostic> out = new ArrayList<>(in.size());
+        for (Diagnostic d : in) {
+            if (d.getMessage() == null || !NOISY_CASCADE.matcher(d.getMessage()).find()) {
+                out.add(d);
+            }
+        }

Review Comment:
   This filter treats any non-type error anywhere in the file as a root cause and then removes every unresolved-type diagnostic in that file. For example, an unrelated broken consequence in one rule hides a genuine unknown type in another rule. Cascade suppression must correlate messages by range/rule/type, or retain the type diagnostic when that correlation is unavailable.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "tkobayas (via GitHub)" <gi...@apache.org>.
tkobayas commented on PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#issuecomment-6140512987

   okay. I'll write a draft and share it with you, @drccrd :)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "tkobayas (via GitHub)" <gi...@apache.org>.
tkobayas commented on PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#issuecomment-6139183686

   > Hi @tkobayas and @drccrd, considering your notable effort on the DRL VsCode extension, I think it would be nice to write a blog post about it in https://kie.apache.org/blog, WDYT? Thanks.
   
   @yesamer A good idea! Shall we schedule the blog post for after the 10.3.0 release, since the editor won't be available until then?


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "drccrd (via GitHub)" <gi...@apache.org>.
drccrd commented on code in PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#discussion_r4166647451


##########
packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/CompileDiagnostics.java:
##########
@@ -0,0 +1,645 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.lsp.server;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import org.drools.completion.DRLDiagnosticHelper;
+import org.drools.completion.WorkspaceScan;
+import org.drools.completion.WorkspaceSiblingResolvers;
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.DiagnosticSeverity;
+import org.eclipse.lsp4j.MessageType;
+import org.eclipse.lsp4j.Range;
+
+/**
+ * Compiles a saved DRL file together with its group through the project's own
+ * engine and keeps the engine's messages per file until the file is edited,
+ * saved again or regrouped. One compile runs at a time; a save during a compile
+ * queues one more and the latest wins.
+ */
+final class CompileDiagnostics {
+
+    static final String SOURCE = "drools";
+    static final String KFS_PREFIX = "src/main/resources/";
+    static final int MAX_DIAGNOSTICS_PER_FILE = 500;
+    static final String NOT_BUILT =
+            "Compile diagnostics need the project's compiled classes. Build the project, then save again.";
+
+    private static final Pattern NOISY_CASCADE = Pattern.compile(
+            "(?i)(unknown\\s*type|cannot\\s*resolve\\s*type|unresolved\\s*type|unable\\s*to\\s*resolve\\s*type|class\\s*not\\s*found)");
+    private static final Logger logger = Logger.getLogger(CompileDiagnostics.class.getName());
+
+    private final DroolsLspServer server;
+    private final Function<Path, String> openText;
+    private final Map<Path, List<Diagnostic>> cached = new ConcurrentHashMap<>();
+    private final AtomicBoolean buildInFlight = new AtomicBoolean(false);
+    private final AtomicReference<PendingBuild> pending = new AtomicReference<>();
+    private final ExecutorService gateExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile-gate"));
+    private final ExecutorService buildExecutor = Executors.newSingleThreadExecutor(daemon("drools-lsp-compile"));
+    private volatile boolean onSave = true;
+    private final AtomicReference<Engine> engine = new AtomicReference<>();
+    private final AtomicBoolean notBuiltNoticeShown = new AtomicBoolean(false);
+
+    CompileDiagnostics(DroolsLspServer server, Function<Path, String> openText) {
+        this.server = server;
+        this.openText = openText;
+    }
+
+    boolean onSave() {
+        return onSave;
+    }
+
+    void setOnSave(boolean onSave) {
+        this.onSave = onSave;
+    }
+
+    Engine engine() {
+        return engine.get();
+    }
+
+    /** Installs the engine; the previous one is closed once the build in flight, if any, has finished. */
+    synchronized void setEngine(Engine next) {
+        if (buildExecutor.isShutdown()) {
+            next.close();
+            return;
+        }
+        Engine previous = engine.getAndSet(next);
+        if (previous == null || previous.available() != next.available()) {
+            logger.info(next.available()
+                    ? "Drools engine found on the project classpath; compile diagnostics are on"
+                    : "No usable Drools engine on the project classpath (needs drools-compiler and drools-mvel); "
+                            + "compile diagnostics are off");
+        }
+        if (previous != null && previous != next) {
+            buildExecutor.submit(previous::close);
+        }
+    }
+
+    /** Releases the engine and stops both executors; the close runs after any build in flight. */
+    synchronized void shutdown() {
+        Engine current = engine.getAndSet(null);
+        if (current != null) {
+            buildExecutor.submit(current::close);
+        }
+        gateExecutor.shutdown();
+        buildExecutor.shutdown();
+    }
+
+    List<Diagnostic> cachedFor(Path path) {
+        return path == null ? List.of() : cached.getOrDefault(normalize(path), List.of());
+    }
+
+    void invalidate(Path path) {
+        if (path != null) {
+            cached.remove(normalize(path));
+        }
+    }
+
+    void invalidateAll() {
+        cached.clear();
+    }
+
+    void onDidSave(Path path, String text) {
+        if (!onSave || path == null || text == null) {
+            return;
+        }
+        trigger(path, text, false);
+    }
+
+    void rebuildWorkspace(Path path) {
+        if (path == null) {
+            return;
+        }
+        String text = openText.apply(normalize(path));
+        if (text == null) {
+            text = readQuietly(path);
+        }
+        if (text == null) {
+            server.showMessage(MessageType.Warning, "Workspace compile skipped: cannot read " + path);
+            return;
+        }
+        trigger(path, text, true);
+    }
+
+    private void trigger(Path path, String text, boolean fullScope) {
+        gateExecutor.submit(() -> triggerNow(path, text, fullScope));
+    }
+
+    void triggerNow(Path path, String text, boolean fullScope) {
+        Engine current = engine.get();
+        if (current == null || !current.available()) {
+            logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: no Drools engine on the project classpath");
+            }
+            return;
+        }
+        if (server.projectClassesMissing()) {
+            if (notBuiltNoticeShown.compareAndSet(false, true)) {
+                logger.info(NOT_BUILT);
+                server.showMessage(MessageType.Info, NOT_BUILT);
+            }
+            if (fullScope) {
+                server.showMessage(MessageType.Warning, "Workspace compile skipped: the project has not been built");
+            }
+            return;
+        }
+        notBuiltNoticeShown.set(false);
+        if (!fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.info("Compile skipped, " + path + " has syntax errors");
+            return;
+        }
+        if (!buildInFlight.compareAndSet(false, true)) {
+            pending.accumulateAndGet(new PendingBuild(path, text, fullScope),
+                    (old, next) -> old != null && old.fullScope && !next.fullScope
+                            ? new PendingBuild(next.path, next.text, true)
+                            : next);
+            drainPendingIfUnclaimed();
+            return;
+        }
+        submit(path, text, fullScope);
+    }
+
+    private void drainPendingIfUnclaimed() {
+        while (pending.get() != null) {
+            if (!buildInFlight.compareAndSet(false, true)) {
+                return;
+            }
+            PendingBuild queued = pending.getAndSet(null);
+            if (queued != null) {
+                submit(queued.path, queued.text, queued.fullScope);
+                return;
+            }
+            buildInFlight.set(false);
+        }
+    }
+
+    private void submit(Path path, String text, boolean fullScope) {
+        buildExecutor.submit(() -> {
+            Engine target = engine.get();
+            boolean updated = false;
+            boolean ran = false;
+            boolean noEngine = false;
+            int errors = 0;
+            int warnings = 0;
+            Compilation result = null;
+            try {
+                if (target == null || !target.available()) {
+                    noEngine = true;
+                    logger.fine(() -> "No Drools engine on the project classpath; not compiling " + path);
+                    return;
+                }
+                if (fullScope) {
+                    server.showMessage(MessageType.Info, "Compiling every DRL file in the workspace");
+                }
+                result = compile(normalize(path), text, fullScope, target);
+                updated = commit(result);
+                errors = result.count(DiagnosticSeverity.Error);
+                warnings = result.count(DiagnosticSeverity.Warning);
+                ran = true;
+            } catch (Throwable t) {
+                logger.log(Level.WARNING, "Drools compile failed for " + path, t);
+                result = failure(normalize(path), text, t);
+                updated = commit(result);
+                errors = 1;
+                ran = true;
+            } finally {
+                if (updated) {
+                    server.refreshDiagnostics();
+                }
+                if (fullScope) {
+                    if (noEngine) {
+                        server.showMessage(MessageType.Warning,
+                                "Workspace compile skipped: no Drools engine on the project classpath");
+                    } else if (ran) {
+                        server.showMessage(errors > 0 ? MessageType.Warning : MessageType.Info,
+                                summaryMessage(errors, warnings, result));
+                    }
+                }
+                buildInFlight.set(false);
+                PendingBuild queued = pending.getAndSet(null);
+                if (queued != null) {
+                    trigger(queued.path, queued.text, queued.fullScope);
+                }
+            }
+        });
+    }
+
+    private static String summaryMessage(int errors, int warnings, Compilation result) {
+        String base = "Workspace compile finished: " + errors + " error(s), " + warnings + " warning(s)";
+        if (errors == 0 || result == null) {
+            return base;
+        }
+        return base + " in " + joinFileNames(result.filesWithErrors());
+    }
+
+    private static String joinFileNames(List<String> files) {
+        StringBuilder names = new StringBuilder();
+        int shown = Math.min(3, files.size());
+        for (int i = 0; i < shown; i++) {
+            if (i > 0) {
+                names.append(", ");
+            }
+            names.append(files.get(i));
+        }
+        if (files.size() > 3) {
+            names.append(" and ").append(files.size() - 3).append(" more");
+        }
+        return names.toString();
+    }
+
+    private Compilation compile(Path saved, String text, boolean fullScope, Engine target) throws Exception {
+        Path root = rootFor(saved);
+        Map<Path, String> sources = new LinkedHashMap<>();
+        Set<Path> skipped = new LinkedHashSet<>();
+        for (Path sibling : scope(saved, fullScope)) {
+            Path normalized = normalize(sibling);
+            if (normalized.equals(saved)) {
+                continue;
+            }
+            String siblingText = openText.apply(normalized);
+            if (siblingText == null) {
+                siblingText = readQuietly(normalized);
+            }
+            if (siblingText == null) {
+                skipped.add(normalized);
+                continue;
+            }
+            if (hasErrors(DRLDiagnosticHelper.parse(siblingText).diagnostics)) {
+                logger.fine(() -> "Leaving " + normalized + " out of the compile, it has syntax errors");
+                skipped.add(normalized);
+                continue;
+            }
+            sources.put(normalized, siblingText);
+        }
+        if (fullScope && hasErrors(DRLDiagnosticHelper.parse(text).diagnostics)) {
+            logger.fine(() -> "Leaving " + saved + " out of the compile, it has syntax errors");
+            skipped.add(saved);
+        } else {
+            sources.put(saved, text);

Review Comment:
   The rebuild is started from the active editor; compiling the file the user invoked it from is intended, even when the workspace enumeration excludes it (which would be a weird use case).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "tkobayas (via GitHub)" <gi...@apache.org>.
tkobayas commented on PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032#issuecomment-5946136267

   @drccrd Thank you very much for this feature!
   
   Here are some review suggestions from Claude. I think they are reasonable points.
   
   ---
   
   1. ProjectEngine.java:127 — worker.join() has no timeout (CONFIRMED)
   
        If the Drools engine hangs (infinite loop, deadlock from a pathological rule pattern), worker.join() blocks the single-threaded build executor forever. All subsequent saves queue but never run — no compile diagnostics until the server is restarted.
   
        Suggested fix: Add a timeout to join() and treat expiry as a build failure. Interrupt the worker to avoid leaking the thread.
   ```
     // Replace:
     worker.join();
     // With:
     worker.join(TimeUnit.SECONDS.toMillis(120));
     if (worker.isAlive()) {
         worker.interrupt();
         throw new RuntimeException("Drools engine build timed out after 30 seconds");
     }
   ```
   [Toshiya] I think "Drools engine hangs during the build" is a very edge case. But it's a good practice to have a timeout.
   
   ---
   
   2. CompileDiagnostics.java:464 — fileFor() suffix matching picks wrong file on name collision (PLAUSIBLE)
   
        When multiple files share the same name (e.g. module-a/Rules.drl and module-b/Rules.drl), the engine may report an error on the bare path Rules.drl. fileFor() iterates fileByKfsPath and key.endsWith("/" + p) returns the first iteration-order match, which may be the wrong file.
   
        Suggested fix: Count matches — if more than one key matches the suffix, fall back to fallback instead of picking an arbitrary one.
   ```
     Path matched = null;
     for (Map.Entry<String, Path> entry : fileByKfsPath.entrySet()) {
         String key = entry.getKey();
         if (p.endsWith("/" + key) || key.endsWith("/" + p) || p.equals(key)) {
             if (matched != null) {
                 return fallback; // ambiguous — don't guess
             }
             matched = entry.getValue();
         }
     }
     return matched != null ? matched : fallback;
   ```
   ---
   
   3. DroolsLspServer.java:185 — Empty classpath leaves stale engine with closed classloader (PLAUSIBLE)
   
        When rebuildClassIndex() receives an empty classpathEntries (e.g. transient Maven resolution failure), it skips the engine replacement block. The previous engine remains installed, but its URLClassLoader was already closed by the prior setEngine() call. The next compile attempt throws from the closed loader.
   
        Suggested fix: When classpath is empty, explicitly clear the engine rather than leaving the stale one in place.
   ```
     if (!classpathEntries.isEmpty()) {
         Set<Path> engineClasspath = new LinkedHashSet<>(classpathEntries);
         engineClasspath.addAll(buildOutputDirs);
         textService.compileDiagnostics().setEngine(ProjectEngine.over(engineClasspath));
     } else {
         textService.compileDiagnostics().setEngine(null);
     }
   ```
   ---
   
   4. BuildMessageRanges.java:96 — Token search hits comments/strings before real code (PLAUSIBLE)
   
        findTokenInWindow searches the original text rather than the already-computed masked text. If a token appears in a comment (e.g. // fix totl field later) before the actual misspelled identifier in a pattern, and the engine reports line 0 (no line info), the diagnostic range points at the comment instead of the code.
   
        Suggested fix: Search masked instead of text for the token position. masked is already computed at line 93 but only used for isInsidePattern/endOfClause.
   ```
     // Line 96: change text to masked
     int hit = findTokenInWindow(masked, token, searchStart, searchEnd);
     // Line 100: same change
     hit = findTokenInWindow(masked, matched, searchStart, searchEnd);
   ```
   ---
   
   5. DroolsLspServer.java:257 — addAll(buildOutputDirs) is a no-op (CONFIRMED)
   
        In setResolvedClasspath(), buildOutputDirs is derived from classpathEntries via filterDirectories() — it is a subset. Adding it back with engineClasspath.addAll(buildOutputDirs) adds nothing. The code works correctly but is redundant and misleading about intent (contrast with rebuildClassIndex() where buildOutputDirs can contain directories not in classpathEntries).
   
        Suggested fix: Remove the redundant addAll.
   ```
     // Replace:
     Set<Path> engineClasspath = new LinkedHashSet<>(classpathEntries);
     engineClasspath.addAll(buildOutputDirs);
     textService.compileDiagnostics().setEngine(ProjectEngine.over(engineClasspath));
     // With:
     textService.compileDiagnostics().setEngine(ProjectEngine.over(classpathEntries));
   ```
   ---
   
   6. DroolsLspDocumentService.java:194 — openTextAt() is O(n) per call, called per sibling per compile (CONFIRMED)
   
        Each call iterates all entries in sourcesMap and normalizes every path via toAbsolutePath().normalize(). For a workspace with 200 open DRL files and a group of 50 siblings, one save triggers 50 × 200 = 10,000 path normalizations. On network mounts, these syscalls add measurable latency to every compile cycle.
   
        Suggested fix: Maintain a reverse lookup map (Path → String) that is updated on didOpen/didClose/didChange, so openTextAt is O(1).
   ```
     // Add field:
     private final Map<Path, String> openTextByPath = new ConcurrentHashMap<>();
   
     // Update in didOpen/didChange:
     Path normalized = toPath(uri).toAbsolutePath().normalize();
     openTextByPath.put(normalized, text);
   
     // Update in didClose:
     openTextByPath.remove(normalized);
   
     // Replace openTextAt:
     private String openTextAt(Path path) {
         return openTextByPath.get(path);
     }
   ```


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org


Re: [PR] incubator-kie-tools#4027: [drl-vscode-extension] compile diagnostics [incubator-kie-tools]

Posted by "tkobayas (via GitHub)" <gi...@apache.org>.
tkobayas merged PR #4032:
URL: https://github.com/apache/incubator-kie-tools/pull/4032


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org