HintLanguageVocabulary.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.sandbox.jdt.triggerpattern.internal;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.sandbox.jdt.triggerpattern.api.GuardFunction;
import org.sandbox.jdt.triggerpattern.api.RewriteActionCatalog;
import org.sandbox.jdt.triggerpattern.api.RewriteActionSchema;
/** Canonical internal vocabulary shared by parser tooling and the editor. */
public final class HintLanguageVocabulary {
/** One metadata/declaration directive and its concise editor documentation. */
public record Directive(String name, String syntax, String description) {
}
/** One schema-validated structured rewrite action. */
public record Action(String name, String replacement, String syntax, String description) {
}
private static final List<Directive> DIRECTIVES= List.of(
new Directive("id", "<!id: rule.id>", "Stable hint-program identifier"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
new Directive("description", "<!description: text>", "Human-readable description"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
new Directive("severity", "<!severity: info|warning|error|hint>", "Reported severity"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
new Directive("minJavaVersion", "<!minJavaVersion: 17>", "Minimum Java source version"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
new Directive("tags", "<!tags: a, b>", "Searchable program tags"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
new Directive("include", "<!include: other.program>", "Compose rules from another program"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
new Directive("caseInsensitive", "<!caseInsensitive>", "Case-insensitive literal matching"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
new Directive("suppressWarnings", "<!suppressWarnings: key>", "Recognized suppression keys"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
new Directive("treeKind", "<!treeKind: METHOD_DECLARATION>", "AST kinds considered by the matcher"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
new Directive("binding-policy", "<!binding-policy: optional|required>", //$NON-NLS-1$ //$NON-NLS-2$
"Compatibility or fail-closed handling of unresolved bindings in ordinary hints"), //$NON-NLS-1$
new Directive("requires-plan", "<!requires-plan: contract-id>",
"Required semantic-plan contract; also implies fail-closed semantic bindings"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
new Directive("foreach", "<!foreach NAME: source -> target>", "Generate rules from key/value pairs"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
new Directive("map", "<!map NAME: source => target>", "Reusable source/replacement mapping"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
new Directive("predicate", "<!predicate name($node): guard-expression>", //$NON-NLS-1$ //$NON-NLS-2$
"Named parameterized guard expression; predicates may compose other predicates")); //$NON-NLS-1$
private static final List<Action> ACTIONS= RewriteActionCatalog.standard().schemas().stream()
.sorted(java.util.Comparator.comparing(RewriteActionSchema::name))
.map(HintLanguageVocabulary::toAction)
.toList();
private static final List<String> OPERATORS= List.of("=>!", "=>", "::", ";;", "&&", "||", "!"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$
private static final Map<String, String> DESCRIPTION_OVERRIDES= Map.ofEntries(
Map.entry("plannedRole", "Matched or bound node has the supplied semantic-plan role"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("enclosingPlannedRole", "An enclosing node has the supplied semantic-plan role"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("plannedValue", "Matched or bound node has an equal typed semantic-plan value"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("enclosingPlannedValue", "A node or enclosing declaration has an equal typed plan value"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("plannedNodeValue", "A typed semantic-plan value references the supplied bound node"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("plannedListContains", "A typed semantic-plan list contains the supplied value"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("plannedRelation", "An exact directed semantic-plan relation connects two nodes"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("plannedRelationValue", "A semantic-plan relation has an equal typed attribute"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("plannedOutgoingRelation", "A node has an outgoing semantic-plan relation of a kind"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("plannedIncomingRelation", "A node has an incoming semantic-plan relation of a kind"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("plannedRelationCount", "A node has the exact number of outgoing planned relations"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("referencedIn", "A variable is referenced in another bound node"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("hasNoSideEffect", "Expression is side-effect free"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("sourceVersionBetween", "Source version is within the supplied range"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("genericTypeIs", "Generic type argument at an index matches a type"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("subtypeOf", "Bound node has the supplied type in its resolved supertype hierarchy"), //$NON-NLS-1$ //$NON-NLS-2$
Map.entry("otherwise", "Always true catch-all alternative")); //$NON-NLS-1$ //$NON-NLS-2$
private static final Set<String> BUILT_IN_GUARD_NAMES= loadBuiltInGuardNames();
private HintLanguageVocabulary() {
}
public static List<Directive> directives() {
return DIRECTIVES;
}
public static Set<String> directiveNames() {
return DIRECTIVES.stream().map(Directive::name)
.collect(java.util.stream.Collectors.toUnmodifiableSet());
}
public static List<Action> actions() {
return ACTIONS;
}
public static Set<String> actionNames() {
return ACTIONS.stream().map(Action::name)
.collect(java.util.stream.Collectors.toUnmodifiableSet());
}
public static List<String> operators() {
return OPERATORS;
}
public static String guardDescription(String name) {
return DESCRIPTION_OVERRIDES.getOrDefault(name, humanize(name));
}
public static Set<String> builtInGuardNames() {
return Set.copyOf(BUILT_IN_GUARD_NAMES);
}
private static Set<String> loadBuiltInGuardNames() {
Map<String, GuardFunction> functions= new LinkedHashMap<>();
BuiltInGuardRegistration.registerAll(functions);
return Set.copyOf(functions.keySet());
}
private static Action toAction(RewriteActionSchema schema) {
String replacementArguments= schema.requiredArguments().stream().sorted()
.map(name -> name + "=") //$NON-NLS-1$
.collect(java.util.stream.Collectors.joining(", ")); //$NON-NLS-1$
String documentedRequired= schema.requiredArguments().stream().sorted()
.map(name -> name + "=VALUE") //$NON-NLS-1$
.collect(java.util.stream.Collectors.joining(", ")); //$NON-NLS-1$
String documentedOptional= optionalSyntax(schema, !documentedRequired.isEmpty());
String replacement= schema.name() + "(" + replacementArguments + ")"; //$NON-NLS-1$ //$NON-NLS-2$
String syntax= "=>! " + schema.name() + "(" + documentedRequired + documentedOptional + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
return new Action(schema.name(), replacement, syntax, schema.description());
}
private static String optionalSyntax(RewriteActionSchema schema, boolean followsRequired) {
List<String> optional= schema.optionalArguments().stream().sorted().toList();
StringBuilder result= new StringBuilder();
for (int index= 0; index < optional.size(); index++) {
String prefix= followsRequired || index > 0 ? ", " : ""; //$NON-NLS-1$ //$NON-NLS-2$
result.append('[').append(prefix).append(optional.get(index)).append("=VALUE]"); //$NON-NLS-1$
}
return result.toString();
}
private static String humanize(String name) {
if (name == null || name.isBlank()) {
return "Registered guard or local predicate"; //$NON-NLS-1$
}
String words= name.replaceAll("([a-z0-9])([A-Z])", "$1 $2") //$NON-NLS-1$ //$NON-NLS-2$
.replace('_', ' ').toLowerCase(Locale.ROOT);
return Character.toUpperCase(words.charAt(0)) + words.substring(1);
}
}