This is an automated email from the ASF dual-hosted git repository.
tkobayas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-tools.git
The following commit(s) were added to refs/heads/main by this push:
new 477f5d81132 incubator-kie-tools#4006: [drl-vscode-extension]
deprecated syntax lint (#4007)
477f5d81132 is described below
commit 477f5d81132dd77ed43b63ba6426891caf6390e3
Author: Daniel Clark <[email protected]>
AuthorDate: Mon Oct 19 09:26:42 2026 +0200
incubator-kie-tools#4006: [drl-vscode-extension] deprecated syntax lint
(#4007)
Co-authored-by: Claude Fable 5.1 <[email protected]>
---
packages/drl-vscode-extension/README.md | 64 ++--
packages/drl-vscode-extension/package.json | 12 +
packages/drl-vscode-extension/src/extension.ts | 1 +
.../drl-vscode-extension/syntaxes/drl.tmLanguage | 12 +-
.../org/drools/completion/CustomOperators.java | 140 +++++++
.../org/drools/completion/DRLDiagnosticHelper.java | 14 +-
.../java/org/drools/completion/DRLLintHelper.java | 5 +-
.../drools/completion/DeprecatedConstructs.java | 356 ++++++++++++++++++
.../org/drools/completion/CustomOperatorsTest.java | 104 ++++++
.../completion/DeprecatedConstructsTest.java | 406 +++++++++++++++++++++
.../lsp/server/DroolsLspDocumentService.java | 19 +-
.../org/drools/lsp/server/DroolsLspServer.java | 41 ++-
.../lsp/server/DroolsLspDocumentServiceTest.java | 41 +++
.../org/drools/lsp/server/DroolsLspServerTest.java | 57 +++
14 files changed, 1234 insertions(+), 38 deletions(-)
diff --git a/packages/drl-vscode-extension/README.md
b/packages/drl-vscode-extension/README.md
index bfda8bdeb68..cfacc26186d 100644
--- a/packages/drl-vscode-extension/README.md
+++ b/packages/drl-vscode-extension/README.md
@@ -49,6 +49,7 @@ Language support for [DRL (Drools Rule
Language)](https://kie.apache.org/docs/10
- Lint diagnostics (missing `end`, missing separators, unbalanced parentheses,
etc.)
- Unknown-type lint with typo quick-fix for DRL-declared types
- Compile diagnostics from the project's own Drools engine on save (see
Compile Diagnostics)
+- Deprecated-syntax lint for constructs the DRL10 parser no longer supports,
with migration quick-fixes
### Information
@@ -222,29 +223,30 @@ Saving a `.drl` file compiles it together with the other
files of its group (see
## Extension Settings
-| Setting | Default | Description
|
-| ------------------------------------------- | ---------- |
--------------------------------------------------------------- |
-| `drools.lsp.logLevel` | `INFO` | Server-side log
level |
-| `drools.lsp.compile.onSave` | `true` | Compile the saved
file's group with the project's Drools engine |
-| `drools.lsp.compile.timeoutSeconds` | `120` | Seconds one
compile may run before it is reported as failed |
-| `drools.lsp.grouping` | `{}` | DRL file
grouping, declared inline (see above) |
-| `drools.lsp.lint.missingEnd` | `warning` | Severity for
missing `end` keyword |
-| `drools.lsp.lint.missingSeparator` | `warning` | Severity for
missing constraint separator |
-| `drools.lsp.lint.missingSemicolon` | `warning` | Severity for
missing semicolon in consequence |
-| `drools.lsp.lint.unbalancedParens` | `warning` | Severity for
unbalanced parentheses |
-| `drools.lsp.lint.unknownTypes` | `warning` | Severity for
unrecognized type references |
-| `drools.lsp.lint.mvelPropertyAccess` | `off` | Hint to prefer
property-access style over getter calls in LHS |
-| `drools.lsp.inlayHints.enabled` | `true` | Show inline type
hints for bound variables |
-| `drools.lsp.maven.pomPath` | `""` | Maven POM path(s)
for classpath resolution |
-| `drools.lsp.java.sourcePaths` | `[]` | Extra Java source
roots, beyond those found automatically |
-| `drools.lsp.java.packageFilters` | `[]` | Package prefixes
limiting which Java source types are indexed |
-| `drools.lsp.formatter.lineLength` | `110` | Formatter wrap
trigger (see Formatting) |
-| `drools.lsp.formatter.lineEndings` | `preserve` | `preserve`, `lf`
or `crlf` |
-| `drools.lsp.formatter.parenPadding` | `true` | Space inside
non-empty parentheses |
-| `drools.lsp.formatter.bindingColonSpace` | `false` | Space before a
binding's or declare field's colon |
-| `drools.lsp.formatter.normalizeTerminators` | `true` | Normalize
`import`/`global` `;` and the accumulate separator |
-| `drools.lsp.formatter.alignDeclarations` | `true` | Column-align
`declare` fields and enum constants |
-| `drools.lsp.formatter.headerMetadata` | `indented` | `indented`,
`flush` or `inline` header annotations/attributes |
+| Setting | Default | Description
|
+| ------------------------------------------- | ---------- |
---------------------------------------------------------------- |
+| `drools.lsp.logLevel` | `INFO` | Server-side log
level |
+| `drools.lsp.compile.onSave` | `true` | Compile the saved
file's group with the project's Drools engine |
+| `drools.lsp.compile.timeoutSeconds` | `120` | Seconds one
compile may run before it is reported as failed |
+| `drools.lsp.grouping` | `{}` | DRL file
grouping, declared inline (see above) |
+| `drools.lsp.lint.missingEnd` | `warning` | Severity for
missing `end` keyword |
+| `drools.lsp.lint.missingSeparator` | `warning` | Severity for
missing constraint separator |
+| `drools.lsp.lint.missingSemicolon` | `warning` | Severity for
missing semicolon in consequence |
+| `drools.lsp.lint.unbalancedParens` | `warning` | Severity for
unbalanced parentheses |
+| `drools.lsp.lint.unknownTypes` | `warning` | Severity for
unrecognized type references |
+| `drools.lsp.lint.mvelPropertyAccess` | `off` | Hint to prefer
property-access style over getter calls in LHS |
+| `drools.lsp.lint.deprecated` | `warning` | Severity for
syntax deprecated with the DRL10 parser (see below) |
+| `drools.lsp.inlayHints.enabled` | `true` | Show inline type
hints for bound variables |
+| `drools.lsp.maven.pomPath` | `""` | Maven POM path(s)
for classpath resolution |
+| `drools.lsp.java.sourcePaths` | `[]` | Extra Java source
roots, beyond those found automatically |
+| `drools.lsp.java.packageFilters` | `[]` | Package prefixes
limiting which Java source types are indexed |
+| `drools.lsp.formatter.lineLength` | `110` | Formatter wrap
trigger (see Formatting) |
+| `drools.lsp.formatter.lineEndings` | `preserve` | `preserve`, `lf`
or `crlf` |
+| `drools.lsp.formatter.parenPadding` | `true` | Space inside
non-empty parentheses |
+| `drools.lsp.formatter.bindingColonSpace` | `false` | Space before a
binding's or declare field's colon |
+| `drools.lsp.formatter.normalizeTerminators` | `true` | Normalize
`import`/`global` `;` and the accumulate separator |
+| `drools.lsp.formatter.alignDeclarations` | `true` | Column-align
`declare` fields and enum constants |
+| `drools.lsp.formatter.headerMetadata` | `indented` | `indented`,
`flush` or `inline` header annotations/attributes |
All lint settings accept: `off`, `hint`, `info`, `warning`, `error`.
@@ -259,6 +261,22 @@ that sit elsewhere; each entry is a literal directory
path, absolute or
workspace-relative, not a glob. Both settings are read when the language server
starts, so changing either needs a restart.
+## Deprecated syntax
+
+Drools 10 ships two DRL parsers: the legacy `DRL6` parser it compiles with by
default, and the ANTLR4 `DRL10` parser, enabled with
`-Ddrools.drl.antlr4.parser.enabled=true`, which accepts a slimmer syntax
([apache/incubator-kie#6220](https://github.com/apache/incubator-kie/issues/6220)).
The language server parses with the `DRL10` grammar, so a rule set that still
uses the dropped syntax shows `no viable alternative` errors where the engine
merely logs a deprecation. The lint names the c [...]
+
+| Construct | Example | Quick fix
|
+| ---------------------------------- | ------------------------- |
----------------------- |
+| Half constraint | `age > 18 && < 65` | Repeat the
left operand |
+| Patterns joined with `&&`/`\|\|` | `not( A( ) && B( ) )` | `and` /
`or` |
+| Annotation after an LHS `and`/`or` | `A( ) or @Annot B( )` | none — move
or drop it |
+| `agenda-group` | `agenda-group "g"` |
`ruleflow-group` |
+| Custom operator without its prefix | `name not supersetOf "x"` |
`##supersetOf` |
+
+A recognized construct replaces the parse errors on its line.
`drools.lsp.lint.deprecated` sets the severity; `off` leaves the raw parse
errors in place.
+
+Custom operators are the `drools.evaluator.<id>` entries of
`META-INF/kie.properties.conf` on the project classpath, read where the engine
reads them. The server registers those ids with its parser, so `##id` is
accepted in the editor as it is by the engine; an identifier no configuration
declares stays a parse error. Operators registered from Java code are not seen.
+
## Known Issues
If you find any issues, please report them in [GitHub
Issues](https://github.com/apache/incubator-kie-tools/issues).
diff --git a/packages/drl-vscode-extension/package.json
b/packages/drl-vscode-extension/package.json
index 54876667766..a2e976bbd61 100644
--- a/packages/drl-vscode-extension/package.json
+++ b/packages/drl-vscode-extension/package.json
@@ -272,6 +272,18 @@
"markdownDescription": "Additional Java source roots used to resolve
the project's own types before a build. Every `src/main/java` under the
workspace is found automatically, so this is only for roots that sit elsewhere.
Each entry is a literal directory path, absolute or workspace-relative — not a
glob. Applied when the language server next starts.",
"type": "array"
},
+ "drools.lsp.lint.deprecated": {
+ "default": "warning",
+ "enum": [
+ "off",
+ "hint",
+ "info",
+ "warning",
+ "error"
+ ],
+ "markdownDescription": "Severity for DRL syntax deprecated for
removal with the DRL10 parser (apache/incubator-kie#6220): half constraints
(`age > 18 && < 65`), patterns joined with `&&`/`||` instead of `and`/`or`,
annotations inside LHS patterns, `agenda-group`, and custom operators without a
`##` prefix. `off` leaves the raw parse errors in place.",
+ "type": "string"
+ },
"drools.lsp.lint.missingEnd": {
"default": "warning",
"description": "Severity for the missing 'end' keyword lint check.",
diff --git a/packages/drl-vscode-extension/src/extension.ts
b/packages/drl-vscode-extension/src/extension.ts
index f49346df906..d6f07c0b0ed 100644
--- a/packages/drl-vscode-extension/src/extension.ts
+++ b/packages/drl-vscode-extension/src/extension.ts
@@ -143,6 +143,7 @@ export async function activate(context:
vscode.ExtensionContext) {
"drools.lsp.lint.unbalancedParens",
"drools.lsp.lint.unknownTypes",
"drools.lsp.lint.mvelPropertyAccess",
+ "drools.lsp.lint.deprecated",
];
for (const prop of lintProps) {
const value: string | undefined = config.get(prop);
diff --git a/packages/drl-vscode-extension/syntaxes/drl.tmLanguage
b/packages/drl-vscode-extension/syntaxes/drl.tmLanguage
index 72c75d63b8d..167f10ecbcf 100644
--- a/packages/drl-vscode-extension/syntaxes/drl.tmLanguage
+++ b/packages/drl-vscode-extension/syntaxes/drl.tmLanguage
@@ -12,9 +12,9 @@
<array>
<dict>
<key>comment</key>
- <string>Mathmatical operators</string>
+ <string>Comparison and logical operators</string>
<key>match</key>
- <string>[<|>|==|<=|>=|.|!=]</string>
+
<string>(==|!=|<=|>=|<|>|&&|\|\|)</string>
<key>name</key>
<string>keyword.operator.source.drools</string>
</dict>
@@ -42,6 +42,14 @@
<key>name</key>
<string>keyword.control.source.drools</string>
</dict>
+ <dict>
+ <key>comment</key>
+ <string>Word operators, built in and custom
(##supersetOf), colored like the other DRL keywords</string>
+ <key>match</key>
+
<string>##[A-Za-z_][A-Za-z0-9_]*|\b(matches|contains|excludes|memberOf|soundslike|str|after|before|coincides|during|finishes|finishedby|includes|meets|metby|overlaps|overlappedby|starts|startedby)\b</string>
+ <key>name</key>
+ <string>keyword.control.source.drools</string>
+ </dict>
<dict>
<key>comment</key>
<string>Bound variables like $person, $myList</string>
diff --git
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/CustomOperators.java
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/CustomOperators.java
new file mode 100644
index 00000000000..9e9a1cfeb84
--- /dev/null
+++
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/CustomOperators.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.completion;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import org.drools.drl.parser.impl.Operator;
+
+/**
+ * The project's custom DRL operators, read from where the engine reads them:
+ * the {@code drools.evaluator.<id>} keys of {@code
META-INF/kie.properties.conf}
+ * and {@code META-INF/kie.default.properties.conf} on its classpath
+ * ({@code org.kie.internal.utils.ChainedProperties}). The engine instantiates
+ * each configured class, whose static initializer registers the operator; only
+ * the keys are read here, so no project code runs in the server.
+ *
+ * <p>Each id is registered in the parser's {@link Operator} cache, which the
+ * DRL10 grammar consults through {@code isPluggableEvaluator} before accepting
+ * {@code ##id} — so a custom operator parses in the editor as it does in the
+ * engine. Operators registered from Java rather than configured, and ids a
+ * definition registers beyond its property key, are not seen.
+ */
+public final class CustomOperators {
+
+ private static final Logger logger =
Logger.getLogger(CustomOperators.class.getName());
+
+ static final String PROPERTY_PREFIX = "drools.evaluator.";
+
+ static final String[] CONF_FILES = {
+ "META-INF/kie.properties.conf",
"META-INF/kie.default.properties.conf"};
+
+ /** Legacy keys the engine's own configuration skips as well. */
+ private static final Set<String> IGNORED_IDS = Set.of("equality",
"comparable");
+
+ private CustomOperators() {
+ }
+
+ /** Registers every operator id declared on {@code classpathEntries};
returns the ids found. */
+ public static Set<String> register(Set<Path> classpathEntries) {
+ Set<String> ids = discover(classpathEntries);
+ for (String id : ids) {
+ Operator.addOperatorToRegistry(id, false);
+ Operator.addOperatorToRegistry(id, true);
+ }
+ return ids;
+ }
+
+ /** Whether {@code id} is an operator the parser accepts after {@code ##}.
*/
+ public static boolean isRegistered(String id) {
+ return id != null && Operator.determineOperator(id, false) != null;
+ }
+
+ /** The operator ids declared on {@code classpathEntries}, without
registering them. */
+ static Set<String> discover(Set<Path> classpathEntries) {
+ Set<String> ids = new TreeSet<>();
+ if (classpathEntries == null) {
+ return ids;
+ }
+ for (Path entry : classpathEntries) {
+ try {
+ if (Files.isDirectory(entry)) {
+ collectFromDirectory(entry, ids);
+ } else if (Files.isRegularFile(entry) &&
entry.toString().endsWith(".jar")) {
+ collectFromJar(entry, ids);
+ }
+ } catch (IOException | RuntimeException e) {
+ logger.log(Level.FINE, "Skipping operator configuration in " +
entry, e);
+ }
+ }
+ return ids;
+ }
+
+ private static void collectFromDirectory(Path dir, Set<String> into)
throws IOException {
+ for (String confFile : CONF_FILES) {
+ Path file = dir.resolve(confFile);
+ if (Files.isRegularFile(file)) {
+ try (InputStream in = Files.newInputStream(file)) {
+ collectIds(read(in), into);
+ }
+ }
+ }
+ }
+
+ private static void collectFromJar(Path jarPath, Set<String> into) throws
IOException {
+ try (ZipFile jar = new ZipFile(jarPath.toFile())) {
+ for (String confFile : CONF_FILES) {
+ ZipEntry zipEntry = jar.getEntry(confFile);
+ if (zipEntry != null) {
+ try (InputStream in = jar.getInputStream(zipEntry)) {
+ collectIds(read(in), into);
+ }
+ }
+ }
+ }
+ }
+
+ private static Properties read(InputStream in) throws IOException {
+ Properties properties = new Properties();
+ properties.load(in);
+ return properties;
+ }
+
+ private static void collectIds(Properties properties, Set<String> into) {
+ for (String key : properties.stringPropertyNames()) {
+ if (key.startsWith(PROPERTY_PREFIX)) {
+ String id = key.substring(PROPERTY_PREFIX.length()).trim();
+ if (!id.isEmpty() && !IGNORED_IDS.contains(id)) {
+ into.add(id);
+ }
+ }
+ }
+ }
+}
diff --git
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLDiagnosticHelper.java
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLDiagnosticHelper.java
index 620145e73f3..7a7ec53a67f 100644
---
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLDiagnosticHelper.java
+++
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLDiagnosticHelper.java
@@ -24,6 +24,7 @@ import java.util.Collections;
import java.util.List;
import org.antlr.v4.runtime.BaseErrorListener;
+import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.Lexer;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
@@ -60,8 +61,10 @@ public final class DRLDiagnosticHelper {
/**
* Parses {@code text} once, returning both the compilation unit and the
- * syntax diagnostics. Callers that also need the tree (e.g. the
unknown-type
- * lint) can reuse it instead of re-parsing.
+ * syntax diagnostics, with the errors that mark a deprecated construct
+ * reported as deprecations instead (see {@link DeprecatedConstructs}).
+ * Callers that also need the tree (e.g. the unknown-type lint) can reuse
it
+ * instead of re-parsing.
*/
public static Parsed parse(String text) {
if (text == null || text.isEmpty()) {
@@ -83,7 +86,12 @@ public final class DRLDiagnosticHelper {
parser.removeErrorListeners();
parser.addErrorListener(listener);
- return new Parsed(parser.compilationUnit(), diagnostics);
+ DRL10Parser.CompilationUnitContext compilationUnit =
parser.compilationUnit();
+ // The lint scans the whole document, including any tail the parser's
+ // error recovery left unread.
+ CommonTokenStream tokens = (CommonTokenStream) parser.getTokenStream();
+ tokens.fill();
+ return new Parsed(compilationUnit,
DeprecatedConstructs.reclassify(text, tokens.getTokens(), diagnostics));
}
/**
diff --git
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLLintHelper.java
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLLintHelper.java
index c72f0b3dc1c..921820f7547 100644
---
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLLintHelper.java
+++
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DRLLintHelper.java
@@ -1175,9 +1175,10 @@ public final class DRLLintHelper {
/**
* Resolves the severity for a pass from its system property: {@code off}
* returns null (pass disabled), unknown values fall back to
- * {@code defaultValue}.
+ * {@code defaultValue}. Package-visible so {@link DeprecatedConstructs}
+ * reads its severity the same way.
*/
- private static DiagnosticSeverity severityFor(String property, String
defaultValue) {
+ static DiagnosticSeverity severityFor(String property, String
defaultValue) {
String value = System.getProperty(property, defaultValue)
.trim().toLowerCase();
switch (value) {
diff --git
a/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DeprecatedConstructs.java
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DeprecatedConstructs.java
new file mode 100644
index 00000000000..027a7abaa4a
--- /dev/null
+++
b/packages/drools-lsp/drools-completion/src/main/java/org/drools/completion/DeprecatedConstructs.java
@@ -0,0 +1,356 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.completion;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.antlr.v4.runtime.Token;
+import org.drools.drl.parser.antlr4.DRL10Lexer;
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.DiagnosticSeverity;
+import org.eclipse.lsp4j.Position;
+import org.eclipse.lsp4j.Range;
+
+/**
+ * Recognizes DRL constructs the engine's legacy parser still compiles but has
+ * deprecated for removal — the families {@code
org.drools.drl.parser.lang.ParserHelper}
+ * warns about: half constraints, patterns joined with {@code &&}/{@code ||},
+ * annotations after an LHS {@code and}/{@code or}, {@code agenda-group}, and
+ * custom operators without the {@code ##} prefix DRL10 requires
+ * (apache/incubator-kie#6220). A custom operator is recognized only when the
+ * project declares it — see {@link CustomOperators}.
+ *
+ * <p>The engine's own warnings fire once per family per parse and carry no
+ * position, so they cannot drive a migration. Nor can this grammar's parse
+ * errors: it rejects some of these constructs ({@code age > 18 && < 65}),
+ * reads others as something else without an error
+ * ({@code ref.order > $ref.order && < $x}), and accepts some outright
+ * ({@code not( A() && B() )}). Detection therefore scans the token stream,
+ * which the lexer produces even where the parser fails, and relies on what
+ * each parenthesis opened and on section for precision. A recognized
construct replaces the parse errors on
+ * its line, which describe the same thing less usefully; otherwise the
+ * deprecation is added.
+ *
+ * <p>Severity comes from {@value #PROP_DEPRECATED} (default {@code warning});
+ * {@code off} disables recognition, leaving the raw parse errors in place. At
+ * most one construct is reported per line; a second surfaces once the first is
+ * migrated, which keeps a sweep's output one finding per line rather than one
+ * per recovery attempt.
+ */
+final class DeprecatedConstructs {
+
+ /** System property: severity for deprecated-construct diagnostics. */
+ static final String PROP_DEPRECATED = "drools.lsp.lint.deprecated";
+
+ private static final String SOURCE = "drools-deprecated";
+
+ private static final String REMOVAL = " will be removed in a future
version (LanguageLevel.DRL10)";
+
+ /**
+ * A recognized construct: the message to show, the last token of its range
+ * and, when the migration is mechanical, the text that should replace that
+ * range. A {@code null} replacement means the construct needs a human
+ * decision (moving or dropping an annotation), so no quick fix is offered.
+ */
+ private static final class Finding {
+ final String message;
+ final String replacement;
+ final Token last;
+
+ Finding(String message, String replacement) {
+ this(message, replacement, null);
+ }
+
+ Finding(String message, String replacement, Token last) {
+ this.message = message;
+ this.replacement = replacement;
+ this.last = last;
+ }
+ }
+
+ /** Tokens that bound the left-hand operand when scanning back from an
operator. */
+ private static final Set<Integer> OPERAND_BOUNDARY = Set.of(
+ DRL10Lexer.LPAREN, DRL10Lexer.COMMA, DRL10Lexer.AND,
DRL10Lexer.OR, DRL10Lexer.SEMI);
+
+ /** Relational operators that, with no left operand, form a half
constraint. */
+ private static final Set<Integer> RELATIONAL = Set.of(
+ DRL10Lexer.LT, DRL10Lexer.GT, DRL10Lexer.LE, DRL10Lexer.GE,
+ DRL10Lexer.EQUAL, DRL10Lexer.NOTEQUAL);
+
+ /** Tokens that can end the left-hand side of a custom-operator
comparison. */
+ private static final Set<Integer> OPERAND_END = Set.of(
+ DRL10Lexer.IDENTIFIER, DRL10Lexer.RPAREN);
+
+ private static final Set<Integer> LHS_CONNECTIVE = Set.of(
+ DRL10Lexer.DRL_AND, DRL10Lexer.DRL_OR, DRL10Lexer.AND,
DRL10Lexer.OR);
+
+ /**
+ * Tokens after which a parenthesis at group level groups conditional
+ * elements rather than opening a pattern's constraints or an expression.
+ */
+ private static final Set<Integer> GROUP_OPENER = Set.of(
+ DRL10Lexer.DRL_WHEN, DRL10Lexer.LPAREN,
+ DRL10Lexer.DRL_AND, DRL10Lexer.DRL_OR, DRL10Lexer.AND,
DRL10Lexer.OR,
+ DRL10Lexer.DRL_NOT, DRL10Lexer.DRL_EXISTS, DRL10Lexer.DRL_FORALL,
+ DRL10Lexer.DRL_ACCUMULATE, DRL10Lexer.DRL_ACC,
DRL10Lexer.DRL_COLLECT,
+ DRL10Lexer.DRL_GROUPBY);
+
+ private DeprecatedConstructs() {
+ }
+
+ /**
+ * Returns {@code syntaxDiagnostics} with every error that marks a
deprecated
+ * construct replaced by a diagnostic naming it, and the follow-on errors
on
+ * those lines removed. {@code lexed} is the full token stream the parse of
+ * {@code text} produced. Returns the input unchanged when recognition is
off
+ * or nothing matches.
+ */
+ static List<Diagnostic> reclassify(String text, List<Token> lexed,
List<Diagnostic> syntaxDiagnostics) {
+ List<Diagnostic> syntax = syntaxDiagnostics == null ? List.of() :
syntaxDiagnostics;
+ if (text == null || text.isEmpty()) {
+ return syntax;
+ }
+ DiagnosticSeverity severity =
DRLLintHelper.severityFor(PROP_DEPRECATED, "warning");
+ if (severity == null) {
+ return syntax;
+ }
+ List<Token> tokens = defaultChannel(lexed);
+ if (tokens.isEmpty()) {
+ return syntax;
+ }
+
+ List<Diagnostic> found = new ArrayList<>();
+ Set<Integer> claimedLines = new HashSet<>();
+ boolean inLhs = false;
+ // One entry per open parenthesis: true when it groups conditional
+ // elements, false when it opens a pattern's constraints or an
expression.
+ Deque<Boolean> parens = new ArrayDeque<>();
+ for (int i = 0; i < tokens.size(); i++) {
+ Token token = tokens.get(i);
+ int type = token.getType();
+ if (type == DRL10Lexer.DRL_WHEN) {
+ inLhs = true;
+ parens.clear();
+ } else if (type == DRL10Lexer.DRL_QUERY && !inLhs &&
parens.isEmpty()) {
+ // A query's body is a left-hand side with no `when`; outside
+ // that position `query` is a field name, not a declaration.
+ inLhs = true;
+ } else if (type == DRL10Lexer.DRL_THEN || type ==
DRL10Lexer.DRL_END) {
+ inLhs = false;
+ parens.clear();
+ }
+ boolean atGroupLevel = parens.isEmpty() || parens.peek();
+
+ Finding finding = describe(tokens, i, inLhs, atGroupLevel, text);
+ if (finding != null && claimedLines.add(token.getLine() - 1)) {
+ Diagnostic d = new Diagnostic();
+ d.setSeverity(severity);
+ d.setSource(SOURCE);
+ d.setMessage(finding.message);
+ d.setRange(rangeOf(token, finding.last == null ? token :
finding.last));
+ // The replacement rides in `data` for the quick fix, the same
+ // way the unknown-type lint carries its suggestion.
+ if (finding.replacement != null) {
+ d.setData(finding.replacement);
+ }
+ found.add(d);
+ }
+
+ if (type == DRL10Lexer.LPAREN) {
+ Token previous = i > 0 ? tokens.get(i - 1) : null;
+ parens.push(atGroupLevel && previous != null &&
GROUP_OPENER.contains(previous.getType()));
+ } else if (type == DRL10Lexer.RPAREN && !parens.isEmpty()) {
+ parens.pop();
+ }
+ }
+ if (found.isEmpty()) {
+ return syntax;
+ }
+ for (Diagnostic d : syntax) {
+ if (d.getRange() == null || d.getRange().getStart() == null
+ ||
!claimedLines.contains(d.getRange().getStart().getLine())) {
+ found.add(d);
+ }
+ }
+ return found;
+ }
+
+ /**
+ * Names the deprecated construct at {@code index}, or {@code null} when
the
+ * tokens there are an ordinary parse failure. Order matters: a {@code &&}
+ * before a relational operator is a half constraint, while the same token
+ * after a closing paren joins two patterns.
+ */
+ private static Finding describe(List<Token> tokens, int index, boolean
inLhs,
+ boolean atGroupLevel, String text) {
+ Token token = tokens.get(index);
+ int type = token.getType();
+ Token next = index + 1 < tokens.size() ? tokens.get(index + 1) : null;
+ Token previous = index > 0 ? tokens.get(index - 1) : null;
+ boolean connective = type == DRL10Lexer.AND || type == DRL10Lexer.OR;
+ boolean insideConstraint = inLhs && !atGroupLevel;
+
+ // Half constraint: `age > 18 && < 65`, inside a pattern's constraints.
+ // The fix repeats the operand the preceding comparison used, so
+ // `ref.order > $ref.order && < $x` becomes
+ // `ref.order > $ref.order && ref.order < $x`.
+ if (insideConstraint && connective && next != null &&
RELATIONAL.contains(next.getType())) {
+ String operand = leftOperandOfPreviousComparison(tokens, index,
text);
+ return new Finding("Deprecated: half constraint '" +
token.getText() + " " + next.getText()
+ + "'" + REMOVAL + " — repeat the left operand.",
+ operand == null ? null : token.getText() + " " + operand);
+ }
+
+ // Pattern connective: `Person( ... ) && Account( ... )`, also inside a
+ // group such as `not( A() && B() )`. The innermost parenthesis is the
+ // discriminator: the same tokens in `Person( (age > 1) && (age < 5) )`
+ // or `eval( a && b )` sit inside a constraint or an expression.
+ if (inLhs && atGroupLevel && connective
+ && previous != null && previous.getType() ==
DRL10Lexer.RPAREN) {
+ String replacement = type == DRL10Lexer.AND ? "and" : "or";
+ return new Finding("Deprecated: connecting patterns with '" +
token.getText() + "'"
+ + REMOVAL + " — use '" + replacement + "' instead.",
replacement);
+ }
+
+ // Annotation on an LHS connective: `A() or @Annot B()`. Annotations
+ // inside a constraint and after a pattern remain supported. Moving or
+ // dropping it is a judgement call, so no fix is offered.
+ if (inLhs && atGroupLevel && type == DRL10Lexer.AT
+ && previous != null &&
LHS_CONNECTIVE.contains(previous.getType())) {
+ return new Finding("Deprecated: annotation after '" +
previous.getText() + "' in the LHS"
+ + REMOVAL + "; annotations elsewhere remain supported.",
null);
+ }
+
+ // `agenda-group "x"` in a rule's header or at package level — lexed as
+ // the identifier `agenda`, `-`, `group` because DRL10 has no such
+ // keyword. The string value tells it apart from `agenda - group` in a
+ // function body.
+ if (!inLhs && type == DRL10Lexer.IDENTIFIER &&
"agenda".equals(token.getText())
+ && next != null && next.getType() == DRL10Lexer.SUB
+ && index + 3 < tokens.size()
+ && "group".equals(tokens.get(index + 2).getText())
+ && isStringLiteral(tokens.get(index + 3))) {
+ return new Finding("Deprecated: 'agenda-group'" + REMOVAL
+ + " — use 'ruleflow-group', which the engine treats as the
same group.",
+ "ruleflow-group", tokens.get(index + 2));
+ }
+
+ // Custom operator used without its DRL10 prefix: `name supersetOf "x"`
+ // or `name not supersetOf "x"`. Only an operator the project declares
+ // counts (see CustomOperators): an undeclared identifier between two
+ // operands is just as likely a typo, and stays the parse error it is.
+ Token operand = previous != null && previous.getType() ==
DRL10Lexer.DRL_NOT && index > 1
+ ? tokens.get(index - 2) : previous;
+ if (insideConstraint && type == DRL10Lexer.IDENTIFIER
+ && operand != null && next != null
+ && OPERAND_END.contains(operand.getType())
+ && isOperandStart(next)
+ && CustomOperators.isRegistered(token.getText())) {
+ return new Finding("Deprecated: custom operator '" +
token.getText()
+ + "' requires a '##' prefix in DRL10 — write '##" +
token.getText() + "'.",
+ "##" + token.getText());
+ }
+
+ return null;
+ }
+
+ /**
+ * The source text of the operand on the left of the comparison preceding
+ * {@code connectiveIndex} — the operand a half constraint omitted. Scans
+ * back for that comparison's operator, then for the operand in front of
it,
+ * stopping at a boundary ({@code (}, {@code ,}, {@code &&}, {@code ||},
+ * {@code ;}) so it never reaches into a neighbouring constraint. Returns
+ * {@code null} when the shape is not recognizable, leaving no quick fix
+ * rather than a wrong one.
+ */
+ private static String leftOperandOfPreviousComparison(List<Token> tokens,
int connectiveIndex,
+ String text) {
+ int operator = -1;
+ for (int i = connectiveIndex - 1; i >= 0; i--) {
+ int type = tokens.get(i).getType();
+ if (RELATIONAL.contains(type)) {
+ operator = i;
+ break;
+ }
+ if (OPERAND_BOUNDARY.contains(type)) {
+ return null;
+ }
+ }
+ if (operator <= 0) {
+ return null;
+ }
+ int end = operator - 1;
+ int start = end;
+ for (int i = end; i >= 0; i--) {
+ int type = tokens.get(i).getType();
+ if (OPERAND_BOUNDARY.contains(type) || RELATIONAL.contains(type)) {
+ break;
+ }
+ start = i;
+ }
+ Token first = tokens.get(start);
+ Token last = tokens.get(end);
+ int from = first.getStartIndex();
+ int to = last.getStopIndex() + 1;
+ if (from < 0 || to > text.length() || from >= to) {
+ return null;
+ }
+ // Sliced from the source so dotted paths and bindings keep their exact
+ // spelling rather than being rebuilt from token texts.
+ return text.substring(from, to).trim();
+ }
+
+ /** True when a token can begin the right-hand side of a comparison. */
+ private static boolean isOperandStart(Token token) {
+ int type = token.getType();
+ return type == DRL10Lexer.IDENTIFIER
+ || type == DRL10Lexer.DRL_STRING_LITERAL
+ || type == DRL10Lexer.STRING_LITERAL
+ || type == DRL10Lexer.DECIMAL_LITERAL
+ || type == DRL10Lexer.BOOL_LITERAL;
+ }
+
+ private static boolean isStringLiteral(Token token) {
+ int type = token.getType();
+ return type == DRL10Lexer.DRL_STRING_LITERAL || type ==
DRL10Lexer.STRING_LITERAL;
+ }
+
+ /** Tokens on the default channel; comments and whitespace are irrelevant
here. */
+ private static List<Token> defaultChannel(List<Token> lexed) {
+ List<Token> out = new ArrayList<>();
+ for (Token token : lexed) {
+ if (token.getChannel() == Token.DEFAULT_CHANNEL && token.getType()
!= Token.EOF) {
+ out.add(token);
+ }
+ }
+ return out;
+ }
+
+ private static Range rangeOf(Token first, Token last) {
+ int length = last.getText() == null ? 1 : Math.max(1,
last.getText().length());
+ return new Range(new Position(first.getLine() - 1,
first.getCharPositionInLine()),
+ new Position(last.getLine() - 1, last.getCharPositionInLine()
+ length));
+ }
+}
diff --git
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/CustomOperatorsTest.java
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/CustomOperatorsTest.java
new file mode 100644
index 00000000000..eedaee75d81
--- /dev/null
+++
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/CustomOperatorsTest.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.completion;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The operator cache is JVM-wide and never emptied, so ids registered by one
+ * test stay registered; tests asserting absence use ids nothing here declares.
+ */
+class CustomOperatorsTest {
+
+ static Path classDirDeclaring(Path root, String confFile, String... ids)
throws Exception {
+ Path conf = root.resolve(confFile);
+ Files.createDirectories(conf.getParent());
+ StringBuilder text = new StringBuilder();
+ for (String id : ids) {
+
text.append("drools.evaluator.").append(id).append("=com.example.").append(id).append("Definition\n");
+ }
+ Files.writeString(conf, text.toString());
+ return root;
+ }
+
+ @Test
+ void discoversIdsFromAClassDirectoryAndAJar(@TempDir Path dir) throws
Exception {
+ Path classes = classDirDeclaring(dir.resolve("classes"),
"META-INF/kie.properties.conf",
+ "supersetOf", "equality", "comparable");
+ Path jar = dir.resolve("dep.jar");
+ try (JarOutputStream out = new
JarOutputStream(Files.newOutputStream(jar))) {
+ out.putNextEntry(new
JarEntry("META-INF/kie.default.properties.conf"));
+ out.write("drools.evaluator.soundsSimilar =
com.example.SoundsSimilarDefinition\n"
+ .getBytes());
+ out.closeEntry();
+ }
+
+ assertThat(CustomOperators.discover(Set.of(classes, jar)))
+ .containsExactly("soundsSimilar", "supersetOf");
+ }
+
+ @Test
+ void discoversIdsFromBothConfigurationFilesInOneJar(@TempDir Path dir)
throws Exception {
+ Path jar = dir.resolve("dep.jar");
+ try (JarOutputStream out = new
JarOutputStream(Files.newOutputStream(jar))) {
+ out.putNextEntry(new JarEntry("META-INF/kie.properties.conf"));
+
out.write("drools.evaluator.closeTo=com.example.CloseToDefinition\n".getBytes());
+ out.closeEntry();
+ out.putNextEntry(new
JarEntry("META-INF/kie.default.properties.conf"));
+
out.write("drools.evaluator.farFrom=com.example.FarFromDefinition\n".getBytes());
+ out.closeEntry();
+ }
+
+
assertThat(CustomOperators.discover(Set.of(jar))).containsExactly("closeTo",
"farFrom");
+ }
+
+ @Test
+ void entriesWithoutAConfigurationYieldNothing(@TempDir Path dir) throws
Exception {
+ Path plainDir = Files.createDirectories(dir.resolve("plain"));
+ Path notAJar = Files.writeString(dir.resolve("notes.txt"),
"drools.evaluator.x=y\n");
+
+ assertThat(CustomOperators.discover(Set.of(plainDir, notAJar,
dir.resolve("missing"))))
+ .isEmpty();
+ assertThat(CustomOperators.discover(null)).isEmpty();
+ }
+
+ /** After registration the DRL10 parser accepts {@code ##id}, as the
engine does. */
+ @Test
+ void registeredIdsParseWithThePrefix(@TempDir Path dir) throws Exception {
+ Path classes = classDirDeclaring(dir, "META-INF/kie.properties.conf",
"supersetOf");
+
+
assertThat(CustomOperators.register(Set.of(classes))).containsExactly("supersetOf");
+
+ assertThat(CustomOperators.isRegistered("supersetOf")).isTrue();
+
assertThat(CustomOperators.isRegistered("neverDeclaredAnywhere")).isFalse();
+ assertThat(DRLDiagnosticHelper.parse(
+ "package p;\nrule R when Person( name ##supersetOf \"x\" )
then end\n").diagnostics)
+ .isEmpty();
+ }
+}
diff --git
a/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DeprecatedConstructsTest.java
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DeprecatedConstructsTest.java
new file mode 100644
index 00000000000..56fc71508ad
--- /dev/null
+++
b/packages/drools-lsp/drools-completion/src/test/java/org/drools/completion/DeprecatedConstructsTest.java
@@ -0,0 +1,406 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.drools.completion;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.DiagnosticSeverity;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Deprecated constructs the engine's legacy parser still compiles. Most
+ * fixtures are parse failures for the DRL10 grammar — which is why the raw
+ * message needs replacing — so they also pin that recognition survives
whatever
+ * the parser's recovery does around them. Custom-operator cases declare their
+ * operator the way a project does, through {@code
META-INF/kie.properties.conf}
+ * on a classpath entry; the operator cache is JVM-wide, so the id stays
+ * registered for the rest of the run.
+ */
+class DeprecatedConstructsTest {
+
+ private static final String HEAD =
+ "package p;\ndeclare Person\n name : String\n age : int\nend\n";
+
+ private static void declareCustomOperator(Path dir, String id) throws
Exception {
+ CustomOperators.register(Set.of(
+ CustomOperatorsTest.classDirDeclaring(dir,
"META-INF/kie.properties.conf", id)));
+ }
+
+ @BeforeEach
+ @AfterEach
+ void reset() {
+ System.clearProperty(DeprecatedConstructs.PROP_DEPRECATED);
+ }
+
+ private static List<Diagnostic> diagnose(String drl) {
+ return DRLDiagnosticHelper.parse(drl).diagnostics;
+ }
+
+ private static Diagnostic deprecation(String drl) {
+ List<Diagnostic> found = diagnose(drl).stream()
+ .filter(d -> "drools-deprecated".equals(d.getSource()))
+ .toList();
+ assertThat(found).as("expected exactly one deprecation diagnostic
in:\n" + drl).hasSize(1);
+ return found.get(0);
+ }
+
+ @Test
+ void namesAHalfConstraint() {
+ Diagnostic d = deprecation(HEAD + "rule R when Person( age > 18 && <
65 ) then end\n");
+ assertThat(d.getMessage()).contains("half constraint").contains("&& <")
+ .contains("repeat the left operand");
+ assertThat(d.getSeverity()).isEqualTo(DiagnosticSeverity.Warning);
+ }
+
+ @Test
+ void namesAHalfConstraintAgainstABinding() {
+ Diagnostic d = deprecation(HEAD
+ + "rule R when $p : Person( age > 1 ) Person( age > 18 && <
$p.age ) then end\n");
+ assertThat(d.getMessage()).contains("half constraint");
+ }
+
+ @Test
+ void namesPatternsConnectedWithAnd() {
+ Diagnostic d = deprecation(HEAD
+ + "rule R when Person( age > 1 ) && Person( name != null )
then end\n");
+ assertThat(d.getMessage()).contains("connecting patterns with
'&&'").contains("use 'and'");
+ }
+
+ @Test
+ void namesPatternsConnectedWithOr() {
+ Diagnostic d = deprecation(HEAD
+ + "rule R when Person( age > 1 ) || Person( name != null )
then end\n");
+ assertThat(d.getMessage()).contains("connecting patterns with
'||'").contains("use 'or'");
+ }
+
+ @Test
+ void namesPatternsConnectedInsideAGroup() {
+ assertThat(deprecation(HEAD
+ + "rule R when ( Person( age > 1 ) && Person( name != null ) )
then end\n")
+ .getData()).isEqualTo("and");
+ assertThat(deprecation(HEAD
+ + "rule R when not( Person( age > 1 ) && Person( age < 1 ) )
then end\n")
+ .getData()).isEqualTo("and");
+ assertThat(deprecation(HEAD
+ + "rule R when exists( Person( age > 1 ) || Person( age < 1 )
) then end\n")
+ .getData()).isEqualTo("or");
+ }
+
+ @Test
+ void doesNotFlagConnectivesInsideAnEval() {
+ assertThat(diagnose(HEAD + "rule R when eval( 1 > 0 && 2 > 1 ) then
end\n"))
+ .allSatisfy(d ->
assertThat(d.getSource()).isNotEqualTo("drools-deprecated"));
+ }
+
+ /** The legacy parser warns on an annotation that follows an LHS {@code
and}/{@code or}. */
+ @Test
+ void namesAnAnnotationOnAnLhsConnective() {
+ assertThat(deprecation(HEAD
+ + "rule R when Person( age > 1 ) or @A Person( age < 1 ) then
end\n").getMessage())
+ .contains("annotation after 'or'");
+ assertThat(deprecation(HEAD
+ + "rule R when ( Person( age > 1 ) and @A Person( age < 1 ) )
then end\n").getMessage())
+ .contains("annotation after 'and'");
+ assertThat(deprecation(HEAD
+ + "rule R when ( or @A Person( age > 1 ) Person( age < 1 ) )
then end\n").getMessage())
+ .contains("annotation after 'or'");
+ }
+
+ /**
+ * DRL10 keeps annotations after {@code &&}/{@code ||} inside a
constraint, and
+ * neither parser accepts one at the start of a constraint.
+ */
+ @Test
+ void doesNotFlagAnnotationsInsideAPattern() {
+ for (String fixture : new String[] {
+ "rule R when Person( age > 1 && @A(1) age < 5 ) then end\n",
+ "rule R when Person( @watch(age) age > 1 ) then end\n",
+ }) {
+ assertThat(diagnose(HEAD + fixture)).as(fixture)
+ .allSatisfy(d ->
assertThat(d.getSource()).isNotEqualTo("drools-deprecated"));
+ }
+ }
+
+ @Test
+ void namesAgendaGroup() {
+ Diagnostic d = deprecation(HEAD
+ + "rule R\n agenda-group \"g\"\n when Person( age > 1 ) then
end\n");
+
assertThat(d.getMessage()).contains("'agenda-group'").contains("ruleflow-group");
+ }
+
+ /** DRL10 accepts attributes as top-level statements, applying to every
rule in the package. */
+ @Test
+ void namesAPackageLevelAgendaGroup() {
+ Diagnostic d = deprecation(HEAD
+ + "agenda-group \"g\"\nrule R when Person( age > 1 ) then
end\n");
+ assertThat(d.getMessage()).contains("'agenda-group'");
+ }
+
+ /** A function body is lexed as code, where {@code agenda - group} is a
subtraction. */
+ @Test
+ void doesNotFlagASubtractionInAFunctionBodyAsAgendaGroup() {
+ assertThat(diagnose(HEAD
+ + "function int f(int agenda, int group) { return agenda -
group; }\n"))
+ .allSatisfy(d ->
assertThat(d.getSource()).isNotEqualTo("drools-deprecated"));
+ }
+
+ /** A query's body is a left-hand side without a {@code when}. */
+ @Test
+ void namesConstructsInsideAQuery(@TempDir Path dir) throws Exception {
+ declareCustomOperator(dir, "queryOp");
+
+ assertThat(deprecation(HEAD + "query q() Person( age > 18 && < 65 )
end\n").getMessage())
+ .contains("half constraint");
+ assertThat(deprecation(HEAD + "query q() Person( age > 1 ) && Person(
name != null ) end\n")
+ .getMessage()).contains("connecting patterns with '&&'");
+ assertThat(deprecation(HEAD + "query q() Person( age > 1 ) or @A
Person( age < 1 ) end\n")
+ .getMessage()).contains("annotation after 'or'");
+ assertThat(deprecation(HEAD + "query q() Person( name queryOp \"x\" )
end\n").getMessage())
+ .contains("custom operator 'queryOp'");
+ }
+
+ @Test
+ void doesNotFlagAQuerysParameters() {
+ assertThat(diagnose(HEAD
+ + "query q(String name, int age) Person( this.name == name,
this.age == age ) end\n"))
+ .allSatisfy(d ->
assertThat(d.getSource()).isNotEqualTo("drools-deprecated"));
+ }
+
+ /** {@code query} used as a field name must not restart the section
mid-pattern. */
+ @Test
+ void aQueryKeywordInsideAPatternDoesNotResetTheSection() {
+ assertThat(deprecation(HEAD + "rule R when Person( query > 1 && < 5 )
then end\n").getMessage())
+ .contains("half constraint");
+ }
+
+ @Test
+ void doesNotFlagAgendaGroupInCommentsStringsOrPatterns() {
+ String[] fixtures = {
+ "/* agenda-group \"g\" */\nrule R when Person( age > 1 ) then
end\n",
+ "// agenda-group \"g\"\nrule R when Person( age > 1 ) then end\n",
+ "rule R when Person( name == \"agenda-group \\\"g\\\"\" ) then
end\n",
+ "rule R when Person( agenda-group \"g\" ) then end\n",
+ "query q() Person( agenda-group \"g\" ) end\n",
+ };
+ for (String fixture : fixtures) {
+ assertThat(diagnose(HEAD + fixture)).as(fixture)
+ .allSatisfy(d ->
assertThat(d.getSource()).isNotEqualTo("drools-deprecated"));
+ }
+ }
+
+ @Test
+ void reportsOneConstructPerLine() {
+ assertThat(deprecation(HEAD + "rule R when Person( age > 1 && < 5 && <
3 ) then end\n")
+ .getRange().getStart().getCharacter()).isEqualTo("rule R when
Person( age > 1 ".length());
+ }
+
+ @Test
+ void reportsConstructsOnSeparateLinesSeparately() {
+ assertThat(diagnose(HEAD + "rule R when\n"
+ + " Person( age > 1 && < 5 )\n"
+ + " Person( age > 2 && < 6 )\n"
+ + "then end\n"))
+ .filteredOn(d -> "drools-deprecated".equals(d.getSource()))
+ .hasSize(2);
+ }
+
+ @Test
+ void namesACustomOperatorMissingItsPrefix(@TempDir Path dir) throws
Exception {
+ declareCustomOperator(dir, "customOp");
+
+ Diagnostic d = deprecation(HEAD + "rule R when Person( name customOp
\"x\" ) then end\n");
+ assertThat(d.getMessage()).contains("custom operator
'customOp'").contains("##customOp");
+ }
+
+ /** An identifier no configuration declares is a typo until proven
otherwise. */
+ @Test
+ void doesNotFlagAnUndeclaredIdentifierAsACustomOperator() {
+ assertThat(diagnose(HEAD + "rule R when Person( name undeclaredOp
\"x\" ) then end\n"))
+ .isNotEmpty()
+ .allSatisfy(d ->
assertThat(d.getSource()).isNotEqualTo("drools-deprecated"));
+ }
+
+ /** The construct's line loses its follow-on parse errors; other lines
keep theirs. */
+ @Test
+ void replacesTheParseErrorsOnTheConstructsLine() {
+ List<Diagnostic> all = diagnose(HEAD + "rule R when Person( age > 18
&& < 65 ) then end\n");
+ assertThat(all).allSatisfy(d ->
+ assertThat(d.getSource()).isEqualTo("drools-deprecated"));
+ }
+
+ @Test
+ void severityIsConfigurableAndOffLeavesTheParseErrors() {
+ String drl = HEAD + "rule R when Person( age > 18 && < 65 ) then
end\n";
+
+ System.setProperty(DeprecatedConstructs.PROP_DEPRECATED, "error");
+
assertThat(deprecation(drl).getSeverity()).isEqualTo(DiagnosticSeverity.Error);
+
+ System.setProperty(DeprecatedConstructs.PROP_DEPRECATED, "off");
+ assertThat(diagnose(drl))
+ .isNotEmpty()
+ .allSatisfy(d ->
assertThat(d.getSource()).isNotEqualTo("drools-deprecated"));
+ }
+
+ /** A mistyped constraint is not a deprecation: no operand precedes the
literal. */
+ @Test
+ void doesNotMisreportAMistypedConstraintAsACustomOperator() {
+ assertThat(diagnose(HEAD + "rule R when Person( age 18 ) then end\n"))
+ .as("a missing operator should stay a syntax error")
+ .allSatisfy(d ->
assertThat(d.getSource()).isNotEqualTo("drools-deprecated"));
+ }
+
+ /** Real rules span lines; the error need not land on the construct's
line. */
+ @Test
+ void namesAHalfConstraintInAMultiLineRule() {
+ Diagnostic d = deprecation(HEAD + """
+ rule R
+ when
+ Person(
+ age > 18
+ && < 65,
+ name != null
+ )
+ then
+ end
+ """);
+ assertThat(d.getMessage()).contains("half constraint");
+ }
+
+ /**
+ * A half constraint whose operands are bound paths parses cleanly under
the
+ * DRL10 grammar, so there is no parse error to replace — detection must
not
+ * depend on one, or the construct stays invisible in exactly the files
that
+ * contain it.
+ */
+ @Test
+ void namesAHalfConstraintInADocumentThatParsesCleanly() {
+ String drl = """
+ package p;
+ rule R
+ when
+ $ref : Fact( order > 0 )
+ Fact( ref.order > $ref.order && < $presentOrder )
+ then
+ end
+ """;
+ assertThat(DRLDiagnosticHelper.parse(drl).diagnostics)
+ .as("the grammar accepts this, so any parse error would
invalidate the premise")
+ .allSatisfy(d ->
assertThat(d.getSource()).isEqualTo("drools-deprecated"));
+ assertThat(deprecation(drl).getMessage()).contains("half constraint");
+ }
+
+ /** Parenthesised constraints are not pattern connectives. */
+ @Test
+ void doesNotFlagParenthesisedConstraintsAsPatternConnectives() {
+ assertThat(diagnose(HEAD
+ + "rule R when Person( (age > 1) && (age < 5) ) then end\n"))
+ .as("&& inside a constraint list is ordinary DRL")
+ .isEmpty();
+ }
+
+ /** Rule-level annotations sit outside the when section and are not
deprecated. */
+ @Test
+ void doesNotFlagRuleLevelAnnotations() {
+ assertThat(diagnose(HEAD
+ + "rule R\n @implements(\"X-1\")\n when Person( age > 1 )
then end\n"))
+ .isEmpty();
+ }
+
+ /**
+ * The half-constraint fix repeats the operand the preceding comparison
used,
+ * so applying it to the diagnostic's range yields valid DRL10.
+ */
+ @Test
+ void halfConstraintFixRepeatsTheLeftOperand() {
+ Diagnostic d = deprecation("""
+ package p;
+ rule R
+ when
+ $fact : Fact( order > 0 )
+ Fact( order > $fact.order && < $presentOrder )
+ then
+ end
+ """);
+ assertThat(d.getData()).isEqualTo("&& order");
+ }
+
+ @Test
+ void patternConnectiveFixIsTheKeyword() {
+ assertThat(deprecation(HEAD
+ + "rule R when Person( age > 1 ) && Person( name != null )
then end\n")
+ .getData()).isEqualTo("and");
+ assertThat(deprecation(HEAD
+ + "rule R when Person( age > 1 ) || Person( name != null )
then end\n")
+ .getData()).isEqualTo("or");
+ }
+
+ @Test
+ void customOperatorFixAddsThePrefix(@TempDir Path dir) throws Exception {
+ declareCustomOperator(dir, "customOp");
+
+ assertThat(deprecation(HEAD + "rule R when Person( name customOp \"x\"
) then end\n")
+ .getData()).isEqualTo("##customOp");
+ }
+
+ @Test
+ void negatedCustomOperatorFixAddsThePrefix(@TempDir Path dir) throws
Exception {
+ declareCustomOperator(dir, "negatedOp");
+
+ assertThat(deprecation(HEAD + "rule R when Person( name not negatedOp
\"x\" ) then end\n")
+ .getData()).isEqualTo("##negatedOp");
+ assertThat(diagnose(HEAD + "rule R when Person( name not ##negatedOp
\"x\" ) then end\n"))
+ .isEmpty();
+ }
+
+ /** Since 6.x the engine aliases ruleflow-group to the agenda group of the
same name. */
+ @Test
+ void agendaGroupFixRenamesTheAttribute() {
+ Diagnostic d = deprecation(HEAD + "rule R\n agenda-group \"g\"\n
when Person( age > 1 ) then end\n");
+ assertThat(d.getData()).isEqualTo("ruleflow-group");
+ assertThat(d.getRange().getStart().getCharacter()).isEqualTo(2);
+ assertThat(d.getRange().getEnd().getCharacter()).isEqualTo(2 +
"agenda-group".length());
+ }
+
+ @Test
+ void noFixIsOfferedForAnnotations() {
+ assertThat(deprecation(HEAD + "rule R when Person( age > 1 ) or @A
Person( age < 1 ) then end\n")
+ .getData()).isNull();
+ }
+
+ /** No fix rather than a wrong one when the preceding comparison is
unreadable. */
+ @Test
+ void halfConstraintWithoutAPrecedingComparisonOffersNoFix() {
+ assertThat(deprecation(HEAD + "rule R when Person( && < 65 ) then
end\n").getData())
+ .isNull();
+ }
+
+ @Test
+ void cleanDrlProducesNothing() {
+ assertThat(diagnose(HEAD + "rule R when Person( age > 18 ) then
end\n")).isEmpty();
+ }
+}
diff --git
a/packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/DroolsLspDocumentService.java
b/packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/DroolsLspDocumentService.java
index 7e322c5ae78..20e0fa6f9ed 100644
---
a/packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/DroolsLspDocumentService.java
+++
b/packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/DroolsLspDocumentService.java
@@ -676,6 +676,7 @@ public class DroolsLspDocumentService implements
TextDocumentService {
}
List<Either<Command, CodeAction>> actions =
new ArrayList<>(buildUnknownTypeActions(uri, inRange));
+ actions.addAll(buildDeprecatedConstructActions(uri, inRange));
if (mvelOn) {
actions.addAll(buildPropertyAccessActions(uri, text,
requested));
}
@@ -692,9 +693,25 @@ public class DroolsLspDocumentService implements
TextDocumentService {
*/
static List<Either<Command, CodeAction>> buildUnknownTypeActions(
String uri, List<Diagnostic> diagnostics) {
+ return replacementActions(uri, diagnostics, "drools-type");
+ }
+
+ /**
+ * Builds the migration quick-fixes for deprecated-construct
+ * ({@code drools-deprecated}) diagnostics, whose replacement rides in
+ * {@code data} the same way. A construct whose migration is not mechanical
+ * — an annotation that must move or go — carries none and so offers no
fix.
+ */
+ static List<Either<Command, CodeAction>> buildDeprecatedConstructActions(
+ String uri, List<Diagnostic> diagnostics) {
+ return replacementActions(uri, diagnostics, "drools-deprecated");
+ }
+
+ private static List<Either<Command, CodeAction>> replacementActions(
+ String uri, List<Diagnostic> diagnostics, String source) {
List<Either<Command, CodeAction>> actions = new ArrayList<>();
for (Diagnostic d : diagnostics) {
- if (!"drools-type".equals(d.getSource())) {
+ if (!source.equals(d.getSource())) {
continue;
}
String suggestion = suggestionOf(d.getData());
diff --git
a/packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/DroolsLspServer.java
b/packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/DroolsLspServer.java
index 4aacd8f1233..21605fa6669 100644
---
a/packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/DroolsLspServer.java
+++
b/packages/drools-lsp/drools-lsp-server/src/main/java/org/drools/lsp/server/DroolsLspServer.java
@@ -31,6 +31,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -38,6 +39,7 @@ import java.util.logging.Logger;
import org.drools.completion.ClassIndex;
import org.drools.completion.ClassMemberIndex;
import org.drools.completion.ClasspathTypeMembers;
+import org.drools.completion.CustomOperators;
import org.drools.completion.DRLDeclaredTypeParser;
import org.drools.completion.JavaSourceRoots;
import org.drools.completion.JavaSourceTypeIndex;
@@ -87,6 +89,8 @@ public class DroolsLspServer implements LanguageServer,
LanguageClientAware {
private volatile Set<Path> classpathEntries = Set.of();
private volatile Set<Path> buildOutputDirs = Set.of();
private volatile List<Path> mavenRoots = List.of();
+ private volatile Set<String> jarCustomOperators = Set.of();
+ private volatile Set<String> registeredCustomOperators = Set.of();
private volatile ClassIndex jarClassIndex = ClassIndex.empty();
private volatile ClassMemberIndex classMemberIndex =
ClassMemberIndex.empty();
private volatile JavaSourceTypeIndex javaSourceIndex =
JavaSourceTypeIndex.empty();
@@ -221,6 +225,7 @@ public class DroolsLspServer implements LanguageServer,
LanguageClientAware {
* waiting for the next edit/save.
*/
private void publishClassIndex() {
+ registerCustomOperators();
ClassIndex outputIndex = ClassIndex.build(buildOutputDirs);
ClassIndex merged = ClassIndex.merge(ClassIndex.merge(jarClassIndex,
outputIndex),
ClassIndex.of(javaSourceIndex.classNames()));
@@ -246,12 +251,33 @@ public class DroolsLspServer implements LanguageServer,
LanguageClientAware {
return dependencyClasspathResolved;
}
+ /**
+ * Registers the custom operators declared on the project's own build
output
+ * and on its dependencies, whenever either set may have changed. The
+ * dependency list from {@code mvn dependency:build-classpath} does not
carry
+ * the project's own {@code target/classes}, which is where its
+ * {@code META-INF/kie.properties.conf} lives, so both sets are scanned.
+ * Dependency jars change only when the classpath resolves, so their ids
are
+ * read there and only the class directories are rescanned here.
+ */
+ private void registerCustomOperators() {
+ Set<Path> scanned = filterDirectories(classpathEntries);
+ scanned.addAll(buildOutputDirs);
+ Set<String> operators = new TreeSet<>(jarCustomOperators);
+ operators.addAll(CustomOperators.register(scanned));
+ if (!operators.equals(registeredCustomOperators)) {
+ registeredCustomOperators = operators;
+ logger.info(() -> "Custom DRL operators: " + operators);
+ }
+ }
+
private void setResolvedClasspath(Set<Path> entries) {
this.classpathEntries = entries;
this.dependencyClasspathResolved = true;
this.buildOutputDirs = filterDirectories(entries);
Set<Path> jars = filterJars(entries);
this.jarClassIndex = jars.isEmpty() ? ClassIndex.empty() :
ClassIndex.build(jars);
+ this.jarCustomOperators = CustomOperators.register(jars);
// Member lookup reflects over the full classpath (jars + class dirs)
// lazily — building the index itself loads no classes.
swapMemberIndex(ClassMemberIndex.of(entries));
@@ -286,13 +312,13 @@ public class DroolsLspServer implements LanguageServer,
LanguageClientAware {
}
/**
- * Test-only entry point: runs the same fast-path source-typing steps
- * {@code initialize} runs before the mvn phase (re-read the
- * {@code drools.lsp.java.*} settings, refresh the source index via
- * {@link #refreshJavaSourceIndex}, install it as the member-index
- * fallback, publish the merged class index) without the async plumbing
- * or a real LSP client — so a test exercising the settings path doesn't
- * need to duplicate {@code initialize}'s sys-prop parsing.
+ * Test-only entry point: runs the same fast-path steps {@code initialize}
+ * runs before the mvn phase (re-read the {@code drools.lsp.java.*}
+ * settings, refresh the source index via {@link #refreshJavaSourceIndex},
+ * install it as the member-index fallback, locate the build output
+ * directories, publish the merged class index) without the async plumbing
+ * or a real LSP client — so a test exercising that path doesn't need to
+ * duplicate {@code initialize}'s sys-prop parsing.
*/
void initializeJavaSourceTypingForTest(Path workspaceRoot) {
this.workspaceRootPath = workspaceRoot;
@@ -303,6 +329,7 @@ public class DroolsLspServer implements LanguageServer,
LanguageClientAware {
System.getProperty("drools.lsp.java.packageFilters"));
refreshJavaSourceIndex(workspaceRoot);
swapMemberIndex(ClassMemberIndex.of(classpathEntries));
+ buildOutputDirs =
MavenClasspathResolver.resolveBuildOutputDirs(workspaceRoot);
publishClassIndex();
}
diff --git
a/packages/drools-lsp/drools-lsp-server/src/test/java/org/drools/lsp/server/DroolsLspDocumentServiceTest.java
b/packages/drools-lsp/drools-lsp-server/src/test/java/org/drools/lsp/server/DroolsLspDocumentServiceTest.java
index 723f729ba53..d8fa6d1d1f0 100644
---
a/packages/drools-lsp/drools-lsp-server/src/test/java/org/drools/lsp/server/DroolsLspDocumentServiceTest.java
+++
b/packages/drools-lsp/drools-lsp-server/src/test/java/org/drools/lsp/server/DroolsLspDocumentServiceTest.java
@@ -679,4 +679,45 @@ class DroolsLspDocumentServiceTest {
assertThat(server.getTextDocumentService().formatterOptions().lineLength()).isEqualTo(80);
}
+
+ // ── deprecated constructs ────────────────────────────────────────────
+
+ @Test
+ void deprecatedConstructsReachTheDocumentDiagnostics() {
+ DroolsLspDocumentService service = getDroolsLspDocumentService(
+ "package p;\nrule R when Person( age > 18 && < 65 ) then
end\n");
+
+ assertThat(service.validate("myDocument"))
+ .isNotEmpty()
+ .allSatisfy(d ->
assertThat(d.getSource()).isEqualTo("drools-deprecated"));
+ }
+
+ @Test
+ void buildDeprecatedConstructActionsOffersTheMigrationEdit() {
+ Diagnostic d = new Diagnostic();
+ d.setSource("drools-deprecated");
+ d.setData("&& person.name");
+ d.setRange(new Range(new Position(4, 30), new Position(4, 32)));
+
+ List<Either<Command, CodeAction>> actions =
+
DroolsLspDocumentService.buildDeprecatedConstructActions("myDocument",
List.of(d));
+
+ assertThat(actions).hasSize(1);
+ CodeAction ca = actions.get(0).getRight();
+ assertThat(ca.getTitle()).isEqualTo("Replace with '&& person.name'");
+ assertThat(ca.getEdit().getChanges().get("myDocument"))
+ .singleElement()
+ .satisfies(e -> assertThat(e.getNewText()).isEqualTo("&&
person.name"));
+ }
+
+ /** Constructs with no mechanical migration carry no data, so offer no
fix. */
+ @Test
+ void buildDeprecatedConstructActionsSkipsDiagnosticsWithoutAReplacement() {
+ Diagnostic d = new Diagnostic();
+ d.setSource("drools-deprecated");
+ d.setRange(new Range(new Position(2, 4), new Position(2, 5)));
+
+
assertThat(DroolsLspDocumentService.buildDeprecatedConstructActions("myDocument",
List.of(d)))
+ .isEmpty();
+ }
}
\ No newline at end of file
diff --git
a/packages/drools-lsp/drools-lsp-server/src/test/java/org/drools/lsp/server/DroolsLspServerTest.java
b/packages/drools-lsp/drools-lsp-server/src/test/java/org/drools/lsp/server/DroolsLspServerTest.java
index 6522f8c3981..ca34af7bbaa 100644
---
a/packages/drools-lsp/drools-lsp-server/src/test/java/org/drools/lsp/server/DroolsLspServerTest.java
+++
b/packages/drools-lsp/drools-lsp-server/src/test/java/org/drools/lsp/server/DroolsLspServerTest.java
@@ -27,11 +27,14 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.drools.completion.ClassIndex;
+import org.drools.completion.CustomOperators;
import org.drools.formatter.FormatterOptions;
import org.eclipse.lsp4j.ClientCapabilities;
import org.eclipse.lsp4j.ConfigurationItem;
@@ -78,6 +81,60 @@ class DroolsLspServerTest {
assertThat(index.getMatching("Foo")).contains("com.example.Foo");
}
+ @Test
+ void rebuildClassIndexRegistersTheProjectsCustomOperators() throws
IOException {
+ DroolsLspServer server =
TestHelperMethods.getDroolsLspServerForDocument("");
+ Path conf = tempDir.resolve("META-INF/kie.properties.conf");
+ Files.createDirectories(conf.getParent());
+ Files.writeString(conf,
"drools.evaluator.nearlyEquals=com.example.NearlyEqualsDefinition\n");
+ server.setClasspathEntriesForTest(Set.of(tempDir));
+
+ server.rebuildClassIndex();
+
+ assertThat(CustomOperators.isRegistered("nearlyEquals")).isTrue();
+ }
+
+ @Test
+ void dependencyJarsAreScannedForOperatorsOnlyWhenTheClasspathResolves()
throws IOException {
+ DroolsLspServer server =
TestHelperMethods.getDroolsLspServerForDocument("");
+ Path jar = tempDir.resolve("dep.jar");
+ writeOperatorJar(jar, "jarDeclaredBefore");
+ server.setClasspathEntriesForTest(Set.of(jar));
+ server.rebuildClassIndex();
+ writeOperatorJar(jar, "jarDeclaredAfter");
+
+ server.rebuildClassIndex();
+
+ assertThat(CustomOperators.isRegistered("jarDeclaredBefore")).isTrue();
+ assertThat(CustomOperators.isRegistered("jarDeclaredAfter")).isFalse();
+ }
+
+ private static void writeOperatorJar(Path jar, String id) throws
IOException {
+ try (JarOutputStream out = new
JarOutputStream(Files.newOutputStream(jar))) {
+ out.putNextEntry(new JarEntry("META-INF/kie.properties.conf"));
+ out.write(("drools.evaluator." + id +
"=com.example.Definition\n").getBytes());
+ out.closeEntry();
+ }
+ }
+
+ /**
+ * The project's own declaration lives in its build output, which the
+ * dependency classpath never lists, and it must be known from the first
+ * publish at startup rather than from a later rebuild.
+ */
+ @Test
+ void startupPublishRegistersOperatorsDeclaredInTheProjectsBuildOutput()
throws IOException {
+ DroolsLspServer server =
TestHelperMethods.getDroolsLspServerForDocument("");
+ Files.writeString(tempDir.resolve("pom.xml"), "<project/>");
+ Path conf =
tempDir.resolve("target/classes/META-INF/kie.properties.conf");
+ Files.createDirectories(conf.getParent());
+ Files.writeString(conf,
"drools.evaluator.roughlyEquals=com.example.RoughlyEqualsDefinition\n");
+
+ server.initializeJavaSourceTypingForTest(tempDir);
+
+ assertThat(CustomOperators.isRegistered("roughlyEquals")).isTrue();
+ }
+
@Test
void didChangeWatchedFilesTriggersRebuild() throws Exception {
DroolsLspServer server =
TestHelperMethods.getDroolsLspServerForDocument("");
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]